├── .cargo └── config.toml ├── .github ├── FUNDING.yml └── workflows │ ├── bench.yaml │ └── ci.yaml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bench-requirements.txt ├── benches ├── comparison.rs ├── input.rs └── npf.rs ├── dev-requirements.txt ├── docs ├── _config.yaml ├── _includes │ └── head.html ├── _inline │ ├── day_count_conventions.md │ └── pe │ │ ├── direct_alpha.md │ │ ├── dpi.md │ │ ├── ks_pme.md │ │ ├── ks_pme_flows.md │ │ ├── ln_pme.md │ │ ├── ln_pme_nav.md │ │ ├── m_pme.md │ │ ├── moic.md │ │ ├── pme_plus.md │ │ ├── pme_plus_flows.md │ │ ├── pme_plus_lambda.md │ │ ├── rvpi.md │ │ └── tvpi.md ├── bench │ ├── data.js │ ├── index.html │ ├── main.js │ ├── styles.css │ └── vectorization │ │ ├── index.html │ │ ├── list-in-default-out.png │ │ ├── list-in-list-out.png │ │ ├── ndarray-in-default-out.png │ │ └── ndarray-in-ndarray-out.png ├── functions.md ├── index.md ├── install.md ├── private_equity.md └── static │ └── bench.png ├── pyproject.toml ├── python └── pyxirr │ ├── __init__.py │ ├── _pyxirr.pyi │ ├── pe.pyi │ └── py.typed ├── src ├── broadcasting.rs ├── conversions.rs ├── core │ ├── mod.rs │ ├── models.rs │ ├── optimize.rs │ ├── periodic.rs │ ├── private_equity.rs │ ├── scheduled │ │ ├── day_count.rs │ │ ├── mod.rs │ │ ├── xirr.rs │ │ └── xnfv.rs │ └── utils.rs └── lib.rs └── tests ├── common ├── cases.rs ├── helpers.rs └── mod.rs ├── samples ├── 1938.csv ├── 30-0.csv ├── 30-1.csv ├── 30-10.csv ├── 30-11.csv ├── 30-12.csv ├── 30-13.csv ├── 30-14.csv ├── 30-15.csv ├── 30-16.csv ├── 30-17.csv ├── 30-18.csv ├── 30-19.csv ├── 30-2.csv ├── 30-20.csv ├── 30-21.csv ├── 30-22.csv ├── 30-23.csv ├── 30-24.csv ├── 30-25.csv ├── 30-26.csv ├── 30-27.csv ├── 30-28.csv ├── 30-29.csv ├── 30-3.csv ├── 30-30.csv ├── 30-31.csv ├── 30-32.csv ├── 30-33.csv ├── 30-34.csv ├── 30-35.csv ├── 30-36.csv ├── 30-37.csv ├── 30-38.csv ├── 30-39.csv ├── 30-4.csv ├── 30-40.csv ├── 30-41.csv ├── 30-42.csv ├── 30-43.csv ├── 30-44.csv ├── 30-45.csv ├── 30-46.csv ├── 30-47.csv ├── 30-48.csv ├── 30-5.csv ├── 30-6.csv ├── 30-7.csv ├── 30-8.csv ├── 30-9.csv ├── minus_0_13.csv ├── minus_0_99.csv ├── minus_0_993.csv ├── minus_0_99999.csv ├── random.csv ├── random_100.csv ├── random_1000.csv ├── random_500.csv ├── rw-100.csv ├── rw-1000.csv ├── rw-50.csv ├── rw-500.csv ├── single_redemption.csv ├── unordered.csv ├── xnfv.csv └── zeros.csv ├── test_conversion.rs ├── test_periodic.rs └── test_scheduled.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [profile.release] 2 | opt-level = 3 3 | debug = false 4 | lto = true 5 | codegen-units = 1 6 | 7 | [profile.bench] 8 | opt-level = 3 9 | debug = false 10 | lto = true 11 | codegen-units = 1 12 | 13 | [target.x86_64-apple-darwin] 14 | rustflags = [ 15 | "-C", "link-arg=-undefined", 16 | "-C", "link-arg=dynamic_lookup", 17 | ] 18 | 19 | [target.aarch64-apple-darwin] 20 | rustflags = [ 21 | "-C", "link-arg=-undefined", 22 | "-C", "link-arg=dynamic_lookup", 23 | ] 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: anexen 2 | -------------------------------------------------------------------------------- /.github/workflows/bench.yaml: -------------------------------------------------------------------------------- 1 | name: Benchmark 2 | on: 3 | push: 4 | tags: ['v*'] 5 | paths-ignore: 6 | - 'docs/**' 7 | workflow_dispatch: 8 | inputs: 9 | build: 10 | description: 'Build' 11 | required: true 12 | default: true 13 | 14 | permissions: 15 | contents: write 16 | deployments: write 17 | 18 | jobs: 19 | benchmark: 20 | name: Run Rust benchmark 21 | runs-on: ubuntu-latest 22 | if: ${{ startsWith(github.ref, 'refs/tags/') || github.event.inputs.build }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: 3.11 28 | - uses: actions-rust-lang/setup-rust-toolchain@v1 29 | with: 30 | toolchain: nightly 31 | - run: pip install -r bench-requirements.txt 32 | - name: Run benchmark 33 | run: cargo +nightly bench --bench comparison | tee output.txt 34 | - name: Store benchmark result 35 | uses: benchmark-action/github-action-benchmark@v1 36 | with: 37 | name: Rust Benchmark 38 | tool: 'cargo' 39 | gh-pages-branch: main 40 | gh-repository: github.com/Anexen/pyxirr 41 | benchmark-data-dir-path: docs/bench 42 | output-file-path: output.txt 43 | auto-push: true 44 | github-token: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | tags: ['v*'] 8 | workflow_dispatch: 9 | 10 | env: 11 | MATURIN_VERSION: 1.7.4 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions-rust-lang/setup-rust-toolchain@v1 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: '3.11' 22 | - name: Test no numpy 23 | run: cargo test --release --features nonumpy 24 | - name: Install numpy v1 25 | run: pip install 'numpy>=1,<2' 'pandas>=1,<2' 26 | - name: Test numpy v1 27 | run: cargo test --release 28 | - name: Install numpy v2 29 | run: pip install 'numpy>=2,<3' 'pandas>=2,<3' 30 | - name: Test numpy v2 31 | run: cargo test --release 32 | 33 | build: 34 | if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} 35 | needs: test 36 | strategy: 37 | matrix: 38 | python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 39 | platform: 40 | - os: ubuntu-latest 41 | - os: macos-latest 42 | - os: macos-latest 43 | arch: aarch64 44 | - os: windows-latest 45 | runs-on: ${{ matrix.platform.os }} 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: actions/setup-python@v5 49 | with: 50 | python-version: ${{ matrix.python }} 51 | # allow-prereleases: ${{ matrix.python == "3.13" }} 52 | - name: Build Wheels - Linux 53 | if: matrix.platform.os == 'ubuntu-latest' 54 | uses: PyO3/maturin-action@v1 55 | with: 56 | maturin-version: ${{ env.MATURIN_VERSION }} 57 | manylinux: auto 58 | args: -i python${{ matrix.python }} --release --strip --sdist 59 | 60 | - name: Check that the source distribution installed correctly 61 | if: matrix.platform.os == 'ubuntu-latest' 62 | run: pip install target/wheels/pyxirr-*.tar.gz 63 | 64 | - name: Build Wheels - MacOS [aarch64] 65 | if: ${{ matrix.platform.os == 'macos-latest' && matrix.platform.arch == 'aarch64' }} 66 | uses: PyO3/maturin-action@v1 67 | with: 68 | maturin-version: ${{ env.MATURIN_VERSION }} 69 | args: -i python --release --target aarch64-apple-darwin --strip 70 | 71 | - name: Build Wheels - MacOS [x86_64] 72 | if: ${{ matrix.platform.os == 'macos-latest' && matrix.platform.arch != 'aarch64' }} 73 | uses: PyO3/maturin-action@v1 74 | with: 75 | maturin-version: ${{ env.MATURIN_VERSION }} 76 | args: -i python --release --target universal2-apple-darwin --strip 77 | 78 | - name: Build Wheels - Windows 79 | if: matrix.platform.os == 'windows-latest' 80 | uses: PyO3/maturin-action@v1 81 | with: 82 | maturin-version: ${{ env.MATURIN_VERSION }} 83 | args: -i python --release --strip 84 | 85 | - name: Upload wheels 86 | uses: actions/upload-artifact@v4 87 | with: 88 | name: wheels-${{ matrix.platform.os }}-${{ matrix.platform.arch || 'x86_64' }}-${{ matrix.python }} 89 | path: target/wheels 90 | 91 | linux-cross: 92 | if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} 93 | needs: test 94 | strategy: 95 | fail-fast: false 96 | matrix: 97 | python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 98 | target: [aarch64, armv7, s390x, ppc64le, ppc64] 99 | runs-on: ubuntu-latest 100 | steps: 101 | - uses: actions/checkout@v4 102 | - uses: actions/setup-python@v5 103 | with: 104 | python-version: ${{ matrix.python }} 105 | - uses: PyO3/maturin-action@v1 106 | with: 107 | maturin-version: ${{ env.MATURIN_VERSION }} 108 | target: ${{ matrix.target }} 109 | manylinux: auto 110 | args: --release --strip -i python${{ matrix.python }} 111 | - uses: actions/upload-artifact@v4 112 | with: 113 | name: wheels-${{ matrix.target }}-${{ matrix.python }} 114 | path: target/wheels 115 | 116 | wasm-emscripten: 117 | if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} 118 | needs: test 119 | strategy: 120 | matrix: 121 | # https://pyodide.org/en/stable/project/changelog.html 122 | version: 123 | - python: '3.12' 124 | emscripten: 3.1.58 # pyodide 0.26.* 125 | - python: '3.11' 126 | emscripten: 3.1.46 # pyodide 0.25.* 127 | - python: '3.11' 128 | emscripten: 3.1.45 # pyodide 0.24.* 129 | runs-on: ubuntu-latest 130 | steps: 131 | - uses: actions/checkout@v4 132 | - uses: mymindstorm/setup-emsdk@v14 133 | with: 134 | version: ${{ matrix.version.emscripten }} 135 | - uses: PyO3/maturin-action@v1 136 | with: 137 | maturin-version: ${{ env.MATURIN_VERSION }} 138 | target: wasm32-unknown-emscripten 139 | rust-toolchain: nightly 140 | args: --release --strip -i python${{ matrix.version.python }} 141 | - name: Upload wheels 142 | uses: actions/upload-artifact@v4 143 | with: 144 | name: wheels-wasm-emscripten-${{ matrix.version.emscripten }} 145 | path: target/wheels 146 | 147 | linux-musl: 148 | if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} 149 | needs: test 150 | strategy: 151 | matrix: 152 | target: 153 | - x86_64-unknown-linux-musl 154 | - i686-unknown-linux-musl 155 | - aarch64-unknown-linux-musl 156 | - armv7-unknown-linux-musleabihf 157 | runs-on: ubuntu-latest 158 | steps: 159 | - uses: actions/checkout@v4 160 | - name: Build Wheels - musl 161 | uses: PyO3/maturin-action@v1 162 | with: 163 | maturin-version: ${{ env.MATURIN_VERSION }} 164 | target: ${{ matrix.target }} 165 | manylinux: musllinux_1_2 166 | args: --release --strip -i 3.7 3.8 3.9 3.10 3.11 3.12 3.13 167 | - name: Upload wheels 168 | uses: actions/upload-artifact@v4 169 | with: 170 | name: wheels-musl-${{ matrix.target }} 171 | path: target/wheels 172 | 173 | publish: 174 | if: startsWith(github.ref, 'refs/tags/') 175 | needs: 176 | - build 177 | - linux-cross 178 | - linux-musl 179 | - wasm-emscripten 180 | runs-on: ubuntu-latest 181 | steps: 182 | - uses: actions/download-artifact@v4 183 | with: 184 | pattern: wheels-* 185 | merge-multiple: true 186 | 187 | - run: pip install maturin 188 | 189 | - name: Release 190 | uses: softprops/action-gh-release@v1 191 | with: 192 | files: pyxirr*.whl 193 | env: 194 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 195 | 196 | - name: PyPI publish 197 | env: 198 | MATURIN_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 199 | # wasm excluded because pypi doesn't support wasm wheels yet 200 | run: find . -name 'pyxirr*' -not -name '*wasm*' | xargs -l maturin upload --skip-existing --username __token__ 201 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .mypy_cache 3 | __pycache__ 4 | *.so 5 | .gdb_history 6 | .pyodide-xbuildenv 7 | .venv 8 | .devbox 9 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | reorder_imports = true 3 | imports_granularity = "Crate" 4 | group_imports = "StdExternalCrate" 5 | use_small_heuristics = "Off" 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.10.6] - 2024-10-13 4 | 5 | - IRR initial guess improvements 6 | - Remove "both negative and positive value" requirement for XNPV 7 | - Python 3.13 support 8 | 9 | ## [0.10.5] - 2024-07-25 10 | 11 | - Fixed segfault during conversion of datetime64 array in numpy v2 12 | 13 | ## [0.10.4] - 2024-06-19 14 | 15 | - Solve XIRR analytically for 2 amounts 16 | - Solve IRR analytically for 2-3 amounts 17 | 18 | ## [0.10.3] - 2024-02-04 19 | 20 | - XIRR Performance improvements 21 | - Fixed building from source distribution 22 | - Build wheels for Pyodide (WASM wheels) 23 | 24 | ## [0.10.2] - 2024-01-27 25 | 26 | - (X)IRR Performance improvements 27 | 28 | ## [0.10.1] - 2023-12-08 29 | 30 | - XIRR improvements ([#49](https://github.com/Anexen/pyxirr/pull/49)) 31 | - Handle NAN in utility functions for multi-root analysis 32 | 33 | ## [0.10.0] - 2023-12-03 34 | 35 | - Private Equity functions ([#42](https://github.com/Anexen/pyxirr/issues/42)) 36 | - XNPV/NPV functions now accept rate as multidimensional array 37 | - Explain Multiple IRR problem + provide utility functions for analysis 38 | - XIRR improvements (prevent from values < -1, grid search + brentq as fallback) 39 | 40 | ## [0.9.3] - 2023-10-11 41 | 42 | - IRR improvements ([#47](https://github.com/Anexen/pyxirr/pull/47)) 43 | - Python 3.12 support 44 | 45 | ## [0.9.2] - 2023-06-14 46 | 47 | - XNFV: suppress `InvalidPaymentsError` by passing `silent=True` flag ([#40](https://github.com/Anexen/pyxirr/issues/40)) 48 | 49 | ## [0.9.1] - 2023-06-03 50 | 51 | - CUMPRINC, CUMIPMT functions 52 | - Upgrade maturin to v1 53 | 54 | ## [0.9.0] - 2023-03-12 55 | 56 | - Vectorized versions of numpy-financial functions 57 | - Breaking change: fixed misspelling in keyword-only argument (pmt_at_beginning) 58 | 59 | ## [0.8.1] - 2023-02-21 60 | 61 | - Fix type annotations ([#35](https://github.com/Anexen/pyxirr/issues/35)) 62 | 63 | ## [0.8.0] - 2023-02-20 64 | 65 | - Add support for different day count conventions ([#34](https://github.com/Anexen/pyxirr/pull/34)) 66 | - Upgrade Rust libraries 67 | - Upgrade maturin to v0.14.13 68 | 69 | ## [0.7.3] - 2022-11-05 70 | 71 | - Upgrade Rust libraries 72 | - Update package metadata 73 | 74 | ## [0.7.2] - 2022-01-28 75 | 76 | - Tweaked IRR to prefer rate > 0 ([#24](https://github.com/Anexen/pyxirr/issues/24)) 77 | - All functions now accept date strings in the format yyyy-mm-dd or mm/dd/yyyy 78 | 79 | ## [0.7.1] - 2021-12-03 80 | 81 | - handle XIRR close to -1 (use brentq algorithm as fallback) 82 | 83 | ## [0.7.0] - 2021-12-02 84 | 85 | - Add an ability to suppress `InvalidPaymentsError` by passing `silent=True` flag ([#22](https://github.com/Anexen/pyxirr/issues/22)) 86 | - Release the GIL for rust-only code 87 | - Type hints 88 | - Refactor tests (use `PyCFunction` interface instead of calling functions directly) 89 | - Upgrade dependencies 90 | 91 | ## [0.6.5] - 2021-11-16 92 | 93 | - Support aarch64, armv7, s390x, ppc64le, ppc64 architectures 94 | - Improve IRR calculation 95 | 96 | ## [0.6.4] - 2021-10-15 97 | 98 | - Wheels for python 3.10 99 | - Add Rate, IPMT, PPMT ([#18](https://github.com/Anexen/pyxirr/pull/18)) 100 | - Test against `numpy-financial` when possible 101 | 102 | ## [0.6.3] - 2021-08-17 103 | 104 | - XIRR improvements ([#15](https://github.com/Anexen/pyxirr/pull/15)) 105 | - add more xirr tests 106 | - handle XIRR close to -1 107 | - fix nfv signature; always return None instead of nan 108 | 109 | ## [0.6.2] - 2021-08-06 110 | 111 | - Support Series with DatetimeIndex ([#13](https://github.com/Anexen/pyxirr/pull/13)) 112 | 113 | ## [0.6.1] - 2021-07-28 114 | 115 | - Add NFV, XFV, XNFV ([#11](https://github.com/Anexen/pyxirr/pull/11)) 116 | 117 | ## [0.6.0] - 2021-07-24 118 | 119 | - Add XFV, PMT, NPER ([#8](https://github.com/Anexen/pyxirr/pull/8), [#9](https://github.com/Anexen/pyxirr/pull/9)) 120 | 121 | ## [0.5.2] - 2021-06-04 122 | 123 | - NPV compatibility mode with Excel 124 | - XIRR optimizations 125 | - Improve the docs 126 | 127 | ## [0.5.1] - 2021-05-25 128 | 129 | - Remove pyo3 wrappers from core 130 | - Benchmark: compare with `numpy-financial` 131 | 132 | ## [0.5.0] - 2021-05-24 133 | 134 | - MIRR, FV 135 | - Performance improvements ([#6](https://github.com/Anexen/pyxirr/pull/6)) 136 | - Test without numpy 137 | - Setup Github Action for benchmark ([#5](https://github.com/Anexen/pyxirr/pull/5)) 138 | 139 | ## [0.4.1] - 2021-05-20 140 | 141 | - Add FV 142 | 143 | ## [0.4.0] - 2021-05-20 144 | 145 | - Add IRR & NPV ([#4](https://github.com/Anexen/pyxirr/pull/4)) 146 | - Optimize cargo build profile for speed 147 | - Setup Github actions for testing and publishing 148 | 149 | ## [0.3.1] - 2021-05-16 150 | 151 | - Faster conversion from `numpy` arrays ([#3](https://github.com/Anexen/pyxirr/pull/3)) 152 | 153 | ## [0.3.0] - 2021-05-11 154 | 155 | - Simplify python conversions 156 | - Refactor tests 157 | - Numpy & Pandas support ([#2](https://github.com/Anexen/pyxirr/pull/2)) 158 | 159 | ## [0.2.1] - 2021-05-07 160 | 161 | - Support row-oriented input for xirr 162 | - Add XNPV 163 | - Faster XIRR implementation 164 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyxirr" 3 | version = "0.10.6" 4 | authors = ["Anexen"] 5 | edition = "2021" 6 | description = "Rust-powered collection of financial functions for Python." 7 | readme = "README.md" 8 | homepage = "https://github.com/Anexen/pyxirr" 9 | license = "Unlicense" 10 | keywords = [ 11 | "python", 12 | "fast", 13 | "financial", 14 | "xirr", 15 | "cashflow", 16 | "day count convention", 17 | "PME", 18 | ] 19 | include = [ 20 | "src/**", 21 | "docs/_inline/**", 22 | "Cargo.toml", 23 | "pyproject.toml", 24 | "LICENSE", 25 | ] 26 | 27 | [lib] 28 | name = "pyxirr" 29 | crate-type = ["rlib", "cdylib"] 30 | doctest = false 31 | 32 | [dependencies] 33 | pyo3 = "0.20" 34 | numpy = "0.20" 35 | time = { version = "0.3", features = ["parsing", "macros"] } 36 | ndarray = "0.15" 37 | # num-complex = "0.4" 38 | 39 | [dev-dependencies] 40 | assert_approx_eq = "1.1" 41 | rstest = { version = "0.18.2", default-features = false } 42 | pyo3 = { version = "0.20", features = ["auto-initialize"]} 43 | 44 | [features] 45 | nonumpy = [] 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![rust-lang.org](https://img.shields.io/badge/Made%20with-Rust-red)](https://www.rust-lang.org/) 2 | [![License](https://img.shields.io/github/license/Anexen/pyxirr.svg)](https://github.com/Anexen/pyxirr/blob/master/LICENSE) 3 | [![pypi](https://img.shields.io/pypi/v/pyxirr.svg)](https://pypi.org/project/pyxirr/) 4 | [![versions](https://img.shields.io/pypi/pyversions/pyxirr.svg)](https://pypi.org/project/pyxirr/) 5 | 6 | # PyXIRR 7 | 8 | Rust-powered collection of financial functions. 9 | 10 | PyXIRR stands for "Python XIRR" (for historical reasons), but contains many other financial functions such as IRR, FV, NPV, etc. 11 | 12 | Features: 13 | 14 | - correct 15 | - supports different day count conventions (e.g. ACT/360, 30E/360, etc.) 16 | - works with different input data types (iterators, numpy arrays, pandas DataFrames) 17 | - no external dependencies 18 | - type annotations 19 | - blazingly fast 20 | 21 | # Installation 22 | 23 | ``` 24 | pip install pyxirr 25 | ``` 26 | 27 | > WASM wheels for [pyodide](https://github.com/pyodide/pyodide) are also available, 28 | > but unfortunately are [not supported by PyPI](https://github.com/pypi/warehouse/issues/10416). 29 | > You can find them on the [GitHub Releases](https://github.com/Anexen/pyxirr/releases) page. 30 | 31 | # Benchmarks 32 | 33 | Rust implementation has been tested against existing [xirr](https://pypi.org/project/xirr/) package 34 | (uses [scipy.optimize](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.newton.html) under the hood) 35 | and the [implementation from the Stack Overflow](https://stackoverflow.com/a/11503492) (pure python). 36 | 37 | ![bench](https://raw.githubusercontent.com/Anexen/pyxirr/main/docs/static/bench.png) 38 | 39 | PyXIRR is much faster than the other implementations. 40 | 41 | Powered by [github-action-benchmark](https://github.com/benchmark-action/github-action-benchmark) and [plotly.js](https://github.com/plotly/plotly.js). 42 | 43 | Live benchmarks are hosted on [Github Pages](https://anexen.github.io/pyxirr/bench). 44 | 45 | # Example 46 | 47 | ```python 48 | from datetime import date 49 | from pyxirr import xirr 50 | 51 | dates = [date(2020, 1, 1), date(2021, 1, 1), date(2022, 1, 1)] 52 | amounts = [-1000, 750, 500] 53 | 54 | # feed columnar data 55 | xirr(dates, amounts) 56 | # feed iterators 57 | xirr(iter(dates), (x / 2 for x in amounts)) 58 | # feed an iterable of tuples 59 | xirr(zip(dates, amounts)) 60 | # feed a dictionary 61 | xirr(dict(zip(dates, amounts))) 62 | # dates as strings 63 | xirr(['2020-01-01', '2021-01-01'], [-1000, 1200]) 64 | ``` 65 | 66 | # Multiple IRR problem 67 | 68 | The Multiple IRR problem occurs when the signs of cash flows change more than 69 | once. In this case, we say that the project has non-conventional cash flows. 70 | This leads to situation, where it can have more the one IRR or have no IRR at all. 71 | 72 | PyXIRR addresses the Multiple IRR problem as follows: 73 | 74 | 1. It looks for positive result around 0.1 (the same as Excel with the default guess=0.1). 75 | 2. If it can't find a result, it uses several other attempts and selects the lowest IRR to be conservative. 76 | 77 | Here is an example illustrating how to identify multiple IRRs: 78 | 79 | ```python 80 | import numpy as np 81 | import pyxirr 82 | 83 | # load cash flow: 84 | cf = pd.read_csv("tests/samples/30-22.csv", names=["date", "amount"]) 85 | # check whether the cash flow is conventional: 86 | print(pyxirr.is_conventional_cash_flow(cf["amount"])) # false 87 | 88 | # build NPV profile: 89 | # calculate 50 NPV values for different rates 90 | rates = np.linspace(-0.5, 0.5, 50) 91 | # any iterable, any rates, e.g. 92 | # rates = [-0.5, -0.3, -0.1, 0.1, -0.6] 93 | values = pyxirr.xnpv(rates, cf) 94 | 95 | # print NPV profile: 96 | # NPV changes sign two times: 97 | # 1) between -0.316 and -0.295 98 | # 2) between -0.03 and -0.01 99 | print("NPV profile:") 100 | for rate, value in zip(rates, values): 101 | print(rate, value) 102 | 103 | # plot NPV profile 104 | import pandas as pd 105 | series = pd.Series(values, index=rates) 106 | pd.DataFrame(series[series > -1e6]).assign(zero=0).plot() 107 | 108 | # find points where NPV function crosses zero 109 | indexes = pyxirr.zero_crossing_points(values) 110 | 111 | print("Zero crossing points:") 112 | for idx in indexes: 113 | print("between", rates[idx], "and", rates[idx+1]) 114 | 115 | # XIRR has two results: 116 | # -0.31540826742734207 117 | # -0.028668460065441048 118 | for i, idx in enumerate(indexes, start=1): 119 | rate = pyxirr.xirr(cf, guess=rates[idx]) 120 | npv = pyxirr.xnpv(rate, cf) 121 | print(f"{i}) {rate}; XNPV = {npv}") 122 | ``` 123 | 124 | # More Examples 125 | 126 | ### Numpy and Pandas 127 | 128 | ```python 129 | import numpy as np 130 | import pandas as pd 131 | 132 | # feed numpy array 133 | xirr(np.array([dates, amounts])) 134 | xirr(np.array(dates), np.array(amounts)) 135 | 136 | # feed DataFrame (columns names doesn't matter; ordering matters) 137 | xirr(pd.DataFrame({"a": dates, "b": amounts})) 138 | 139 | # feed Series with DatetimeIndex 140 | xirr(pd.Series(amounts, index=pd.to_datetime(dates))) 141 | 142 | # bonus: apply xirr to a DataFrame with DatetimeIndex: 143 | df = pd.DataFrame( 144 | index=pd.date_range("2021", "2022", freq="MS", inclusive="left"), 145 | data={ 146 | "one": [-100] + [20] * 11, 147 | "two": [-80] + [19] * 11, 148 | }, 149 | ) 150 | df.apply(xirr) # Series(index=["one", "two"], data=[5.09623547168478, 8.780801977141174]) 151 | ``` 152 | 153 | ### Day count conventions 154 | 155 | Check out the available options on the [docs/day-count-conventions](https://anexen.github.io/pyxirr/functions.html#day-count-conventions). 156 | 157 | ```python 158 | from pyxirr import DayCount 159 | 160 | xirr(dates, amounts, day_count=DayCount.ACT_360) 161 | 162 | # parse day count from string 163 | xirr(dates, amounts, day_count="30E/360") 164 | ``` 165 | 166 | ### Private equity performance metrics 167 | 168 | ```python 169 | from pyxirr import pe 170 | 171 | pe.pme_plus([-20, 15, 0], index=[100, 115, 130], nav=20) 172 | 173 | pe.direct_alpha([-20, 15, 0], index=[100, 115, 130], nav=20) 174 | ``` 175 | 176 | [Docs](https://anexen.github.io/pyxirr/private_equity.html) 177 | 178 | ### Other financial functions 179 | 180 | ```python 181 | import pyxirr 182 | 183 | # Future Value 184 | pyxirr.fv(0.05/12, 10*12, -100, -100) 185 | 186 | # Net Present Value 187 | pyxirr.npv(0, [-40_000, 5_000, 8_000, 12_000, 30_000]) 188 | 189 | # IRR 190 | pyxirr.irr([-100, 39, 59, 55, 20]) 191 | 192 | # ... and more! Check out the docs. 193 | ``` 194 | 195 | [Docs](https://anexen.github.io/pyxirr/functions.html) 196 | 197 | ### Vectorization 198 | 199 | PyXIRR supports numpy-like vectorization. 200 | 201 | If all input is scalar, returns a scalar float. If any input is array_like, 202 | returns values for each input element. If multiple inputs are 203 | array_like, performs broadcasting and returns values for each element. 204 | 205 | ```python 206 | import pyxirr 207 | 208 | # feed list 209 | pyxirr.fv([0.05/12, 0.06/12], 10*12, -100, -100) 210 | pyxirr.fv([0.05/12, 0.06/12], [10*12, 9*12], [-100, -200], -100) 211 | 212 | # feed numpy array 213 | import numpy as np 214 | rates = np.array([0.05, 0.06, 0.07])/12 215 | pyxirr.fv(rates, 10*12, -100, -100) 216 | 217 | # feed any iterable! 218 | pyxirr.fv( 219 | np.linspace(0.01, 0.2, 10), 220 | (x + 1 for x in range(10)), 221 | range(-100, -1100, -100), 222 | tuple(range(-100, -200, -10)) 223 | ) 224 | 225 | # 2d, 3d, 4d, and more! 226 | rates = [[[[[[0.01], [0.02]]]]]] 227 | pyxirr.fv(rates, 10*12, -100, -100) 228 | ``` 229 | 230 | # API reference 231 | 232 | See the [docs](https://anexen.github.io/pyxirr) 233 | 234 | # Roadmap 235 | 236 | - [x] Implement all functions from [numpy-financial](https://numpy.org/numpy-financial/latest/index.html) 237 | - [x] Improve docs, add more tests 238 | - [x] Type hints 239 | - [x] Vectorized versions of numpy-financial functions. 240 | - [ ] Compile library for rust/javascript/python 241 | 242 | # Development 243 | 244 | Running tests with pyo3 is a bit tricky. In short, you need to compile your tests without `extension-module` feature to avoid linking errors. 245 | See the following issues for the details: [#341](https://github.com/PyO3/pyo3/issues/341), [#771](https://github.com/PyO3/pyo3/issues/771). 246 | 247 | If you are using `pyenv`, make sure you have the shared library installed (check for `${PYENV_ROOT}/versions//lib/libpython3.so` file). 248 | 249 | ```bash 250 | $ PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 251 | ``` 252 | 253 | Install dev-requirements 254 | 255 | ```bash 256 | $ pip install -r dev-requirements.txt 257 | ``` 258 | 259 | ### Building 260 | 261 | ```bash 262 | $ maturin develop 263 | ``` 264 | 265 | ### Testing 266 | 267 | ```bash 268 | $ LD_LIBRARY_PATH=${PYENV_ROOT}/versions/3.10.8/lib cargo test 269 | ``` 270 | 271 | ### Benchmarks 272 | 273 | ```bash 274 | $ pip install -r bench-requirements.txt 275 | $ LD_LIBRARY_PATH=${PYENV_ROOT}/versions/3.10.8/lib cargo +nightly bench 276 | ``` 277 | 278 | # Building and distribution 279 | 280 | This library uses [maturin](https://github.com/PyO3/maturin) to build and distribute python wheels. 281 | 282 | ```bash 283 | $ docker run --rm -v $(pwd):/io ghcr.io/pyo3/maturin build --release --manylinux 2010 --strip 284 | $ maturin upload target/wheels/pyxirr-${version}* 285 | ``` 286 | -------------------------------------------------------------------------------- /bench-requirements.txt: -------------------------------------------------------------------------------- 1 | scipy==1.* 2 | numpy-financial==1.* 3 | -------------------------------------------------------------------------------- /benches/comparison.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use pyo3::{types::PyModule, Python}; 6 | use test::{black_box, Bencher}; 7 | 8 | #[path = "../tests/common/mod.rs"] 9 | mod common; 10 | use common::PaymentsLoader; 11 | 12 | // https://stackoverflow.com/questions/8919718/financial-python-library-that-has-xirr-and-xnpv-function 13 | const PURE_PYTHON_IMPL: &str = r#" 14 | def xirr(transactions): 15 | years = [(ta[0] - transactions[0][0]).days / 365.0 for ta in transactions] 16 | residual = 1 17 | step = 0.05 18 | guess = 0.05 19 | epsilon = 0.0001 20 | limit = 10000 21 | while abs(residual) > epsilon and limit > 0: 22 | limit -= 1 23 | residual = 0.0 24 | for i, ta in enumerate(transactions): 25 | residual += ta[1] / pow(guess + 1, years[i]) 26 | if abs(residual) > epsilon: 27 | if residual > 0: 28 | guess += step 29 | else: 30 | guess -= step 31 | step /= 2.0 32 | return guess 33 | "#; 34 | 35 | const SCIPY_IMPL: &str = r#" 36 | import scipy.optimize 37 | 38 | def xnpv(rate, values, dates): 39 | if rate <= -1.0: 40 | return float('inf') 41 | d0 = dates[0] # or min(dates) 42 | return sum([ vi / (1.0 + rate)**((di - d0).days / 365.0) for vi, di in zip(values, dates)]) 43 | 44 | def xirr(values, dates): 45 | try: 46 | return scipy.optimize.newton(lambda r: xnpv(r, values, dates), 0.0) 47 | except RuntimeError: # Failed to converge? 48 | return scipy.optimize.brentq(lambda r: xnpv(r, values, dates), -1.0, 1e10) 49 | "#; 50 | 51 | macro_rules! bench_rust { 52 | ($name:ident, $file:expr) => { 53 | #[bench] 54 | fn $name(b: &mut Bencher) { 55 | Python::with_gil(|py| { 56 | let data = PaymentsLoader::from_csv(py, $file).to_records(); 57 | b.iter(|| pyxirr_call_impl!(py, "xirr", black_box((data,))).unwrap()); 58 | }); 59 | } 60 | }; 61 | } 62 | 63 | macro_rules! bench_scipy { 64 | ($name:ident, $file:expr) => { 65 | #[bench] 66 | fn $name(b: &mut Bencher) { 67 | Python::with_gil(|py| { 68 | let xirr = PyModule::from_code(py, SCIPY_IMPL, "xirr.py", "scipy_py_xirr") 69 | .unwrap() 70 | .getattr("xirr") 71 | .unwrap(); 72 | let data = PaymentsLoader::from_csv(py, $file).to_columns(); 73 | b.iter(|| xirr.call1(black_box((data.1, data.0))).unwrap()) 74 | }); 75 | } 76 | }; 77 | } 78 | 79 | macro_rules! bench_python { 80 | ($name:ident, $file:expr) => { 81 | #[bench] 82 | fn $name(b: &mut Bencher) { 83 | Python::with_gil(|py| { 84 | let xirr = PyModule::from_code(py, PURE_PYTHON_IMPL, "xirr.py", "pure_py_xirr") 85 | .unwrap() 86 | .getattr("xirr") 87 | .unwrap(); 88 | let data = PaymentsLoader::from_csv(py, $file).to_records(); 89 | b.iter(|| xirr.call1(black_box((data,))).unwrap()) 90 | }); 91 | } 92 | }; 93 | } 94 | 95 | bench_rust!(bench_rust_50, "tests/samples/rw-50.csv"); 96 | bench_rust!(bench_rust_100, "tests/samples/rw-100.csv"); 97 | bench_rust!(bench_rust_500, "tests/samples/rw-500.csv"); 98 | bench_rust!(bench_rust_1000, "tests/samples/rw-1000.csv"); 99 | 100 | bench_scipy!(bench_scipy_50, "tests/samples/rw-50.csv"); 101 | bench_scipy!(bench_scipy_100, "tests/samples/rw-100.csv"); 102 | bench_scipy!(bench_scipy_500, "tests/samples/rw-500.csv"); 103 | bench_scipy!(bench_scipy_1000, "tests/samples/rw-1000.csv"); 104 | 105 | bench_python!(bench_python_50, "tests/samples/rw-50.csv"); 106 | bench_python!(bench_python_100, "tests/samples/rw-100.csv"); 107 | bench_python!(bench_python_500, "tests/samples/rw-500.csv"); 108 | bench_python!(bench_python_1000, "tests/samples/rw-1000.csv"); 109 | -------------------------------------------------------------------------------- /benches/input.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use pyo3::Python; 6 | use test::Bencher; 7 | 8 | #[path = "../tests/common/mod.rs"] 9 | mod common; 10 | 11 | #[bench] 12 | fn bench_from_records(b: &mut Bencher) { 13 | Python::with_gil(|py| { 14 | let input = "tests/samples/random_100.csv"; 15 | let data = common::PaymentsLoader::from_csv(py, input).to_records(); 16 | b.iter(|| pyxirr_call_impl!(py, "xirr", (data,)).unwrap()); 17 | }); 18 | } 19 | 20 | #[bench] 21 | fn bench_from_columns(b: &mut Bencher) { 22 | Python::with_gil(|py| { 23 | let input = "tests/samples/random_100.csv"; 24 | let data = common::PaymentsLoader::from_csv(py, input).to_columns(); 25 | b.iter(|| pyxirr_call_impl!(py, "xirr", data).unwrap()); 26 | }); 27 | } 28 | 29 | #[bench] 30 | fn bench_from_dict(b: &mut Bencher) { 31 | Python::with_gil(|py| { 32 | let input = "tests/samples/random_100.csv"; 33 | let data = common::PaymentsLoader::from_csv(py, input).to_dict(); 34 | b.iter(|| pyxirr_call_impl!(py, "xirr", (data,)).unwrap()); 35 | }); 36 | } 37 | 38 | #[bench] 39 | fn bench_from_pandas(b: &mut Bencher) { 40 | Python::with_gil(|py| { 41 | let input = "tests/samples/random_100.csv"; 42 | let data = common::pd_read_csv(py, input); 43 | b.iter(|| pyxirr_call_impl!(py, "xirr", (data,)).unwrap()); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /benches/npf.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use pyo3::{types::*, Python}; 6 | use test::{black_box, Bencher}; 7 | 8 | #[path = "../tests/common/mod.rs"] 9 | mod common; 10 | 11 | static B_1: &[i32] = &[-100, 39, 59, 55, 20]; 12 | 13 | #[bench] 14 | fn bench_1_irr(b: &mut Bencher) { 15 | Python::with_gil(|py| { 16 | let payments = PyList::new(py, B_1); 17 | b.iter(|| black_box(pyxirr_call_impl!(py, "irr", (payments,)).unwrap())); 18 | }); 19 | } 20 | 21 | #[bench] 22 | fn bench_1_irr_npf(b: &mut Bencher) { 23 | Python::with_gil(|py| { 24 | let irr = py.import("numpy_financial").unwrap().getattr("irr").unwrap(); 25 | let payments = PyList::new(py, B_1); 26 | b.iter(|| black_box(irr.call1((payments,)).unwrap())) 27 | }); 28 | } 29 | 30 | static B_2: &[f64] = &[ 31 | -217500.0, 32 | -217500.0, 33 | 108466.80462450592, 34 | 101129.96439328062, 35 | 93793.12416205535, 36 | 86456.28393083003, 37 | 79119.44369960476, 38 | 71782.60346837944, 39 | 64445.76323715414, 40 | 57108.92300592884, 41 | 49772.08277470355, 42 | 42435.24254347826, 43 | 35098.40231225296, 44 | 27761.56208102766, 45 | 20424.721849802358, 46 | 13087.88161857707, 47 | 5751.041387351768, 48 | -1585.7988438735192, 49 | -8922.639075098821, 50 | -16259.479306324123, 51 | -23596.31953754941, 52 | -30933.159768774713, 53 | -38270.0, 54 | -45606.8402312253, 55 | -52943.680462450604, 56 | -60280.520693675906, 57 | -67617.36092490121, 58 | ]; 59 | 60 | #[bench] 61 | fn bench_2_irr(b: &mut Bencher) { 62 | Python::with_gil(|py| { 63 | let payments = PyList::new(py, B_2); 64 | b.iter(|| black_box(pyxirr_call_impl!(py, "irr", (payments,)).unwrap())); 65 | }); 66 | } 67 | 68 | #[bench] 69 | fn bench_2_irr_npf(b: &mut Bencher) { 70 | Python::with_gil(|py| { 71 | let irr = py.import("numpy_financial").unwrap().getattr("irr").unwrap(); 72 | let payments = PyList::new(py, B_2); 73 | b.iter(|| black_box(irr.call1((payments,)).unwrap())) 74 | }); 75 | } 76 | 77 | static B_3: &[f64] = &[10.0, 1.0, 2.0, -3.0, 4.0]; 78 | 79 | #[bench] 80 | fn bench_3_irr_none(b: &mut Bencher) { 81 | Python::with_gil(|py| { 82 | let payments = PyList::new(py, B_3); 83 | b.iter(|| black_box(pyxirr_call_impl!(py, "irr", (payments,)).unwrap())); 84 | }); 85 | } 86 | 87 | #[bench] 88 | fn bench_3_irr_none_npf(b: &mut Bencher) { 89 | Python::with_gil(|py| { 90 | let irr = py.import("numpy_financial").unwrap().getattr("irr").unwrap(); 91 | let payments = PyList::new(py, B_3); 92 | b.iter(|| black_box(irr.call1((payments,)).unwrap())) 93 | }); 94 | } 95 | 96 | static B_4: &[i32] = &[-1000, 100, 250, 500, 500]; 97 | 98 | #[bench] 99 | fn bench_4_mirr(b: &mut Bencher) { 100 | Python::with_gil(|py| { 101 | let values = PyList::new(py, B_4); 102 | b.iter(|| black_box(pyxirr_call_impl!(py, "mirr", (values, 0.1, 0.1)).unwrap())) 103 | }); 104 | } 105 | 106 | #[bench] 107 | fn bench_4_mirr_npf(b: &mut Bencher) { 108 | Python::with_gil(|py| { 109 | let mirr = py.import("numpy_financial").unwrap().getattr("mirr").unwrap(); 110 | let values = PyList::new(py, B_4); 111 | b.iter(|| black_box(mirr.call1((values, 0.1, 0.1)).unwrap())) 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | maturin==1.4.* 2 | numpy==1.* 3 | pandas==1.* 4 | numpy-financial==1.* 5 | -------------------------------------------------------------------------------- /docs/_config.yaml: -------------------------------------------------------------------------------- 1 | name: Rust-powered collection of financial functions 2 | title: null 3 | -------------------------------------------------------------------------------- /docs/_includes/head.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /docs/_inline/day_count_conventions.md: -------------------------------------------------------------------------------- 1 | The [day count convention](https://en.wikipedia.org/wiki/Day_count_convention) 2 | determines how interest accrues over time in a variety of transactions, 3 | including bonds, swaps, bills and loans. 4 | 5 | The following conventions are supported: 6 | 7 | | Name | Constant | Also known | 8 | | ------------------ | -------------------------- | ------------------------------- | 9 | | Actual/Actual ISDA | DayCount.ACT_ACT_ISDA | Act/Act ISDA | 10 | | Actual/365 Fixed | DayCount.ACT_365F | Act/365F, English | 11 | | Actual/365.25 | DayCount.ACT_365_25 | | 12 | | Actual/364 | DayCount.ACT_364 | | 13 | | Actual/360 | DayCount.ACT_360 | French | 14 | | 30/360 ISDA | DayCount.THIRTY_360_ISDA | Bond basis | 15 | | 30E/360 | DayCount.THIRTY_E_360 | 30/360 ISMA, Eurobond basis | 16 | | 30E+/360 | DayCount.THIRTY_E_PLUS_360 | | 17 | | 30E/360 ISDA | DayCount.THIRTY_E_360_ISDA | 30E/360 German, German | 18 | | 30U/360 | DayCount.THIRTY_U_360 | 30/360 US, 30US/360, 30/360 SIA | 19 | | NL/365 | DayCount.NL_365 | Actual/365 No leap year | 20 | | NL/360 | DayCount.NL_360 | | 21 | 22 | See also: 23 | 24 | - [2006 ISDA definitions](https://www.rbccm.com/assets/rbccm/docs/legal/doddfrank/Documents/ISDALibrary/2006%20ISDA%20Definitions.pdf) 25 | - http://www.deltaquants.com/day-count-conventions 26 | -------------------------------------------------------------------------------- /docs/_inline/pe/direct_alpha.md: -------------------------------------------------------------------------------- 1 | The concept of direct alpha is closely related to the KS PME as it uses the 2 | same method to calculate an IRR which is then compared to the fund’s IRR. 3 | However, the difference between KS PME and direct alpha method is that in 4 | direct alpha the under or out performance is quantified by calculating the 5 | compounded cash flows plus the fund’s NAV. 6 | 7 | See also: 8 | - 9 | - 10 | -------------------------------------------------------------------------------- /docs/_inline/pe/dpi.md: -------------------------------------------------------------------------------- 1 | Distributed to Paid-In Capital (DPI) is a term used to measure the total 2 | capital that a private equity fund has returned thus far to its investors. It 3 | is also referred to as the realisation multiple. The DPI value is the 4 | cumulative value of all investor distributions expressed as a multiple of all 5 | the capital paid into the fund up to that time. 6 | 7 | Formula: DPI = Cumulative Distributions / Paid-In Capital 8 | 9 | See also: 10 | - 11 | - 12 | -------------------------------------------------------------------------------- /docs/_inline/pe/ks_pme.md: -------------------------------------------------------------------------------- 1 | KS PME represents a market adjusted equivalent of the Total Value to 2 | Paid-In-Capital (TVPI). KS PME is calculated by finding the future value of 3 | each contribution and distribution using the stock market index returns. 4 | 5 | Formula: FV(Distributions) + NAV / FV(Contributions) 6 | 7 | See also: 8 | - `ks_pme_flows` and `tvpi` functions 9 | - 10 | - 11 | -------------------------------------------------------------------------------- /docs/_inline/pe/ks_pme_flows.md: -------------------------------------------------------------------------------- 1 | Use the Kaplan-Schoar method to re-scale the private equity flows to match the 2 | public market equivalents (PME) for comparison. 3 | 4 | This method works as follows, for each period, re-scale the amount as: 5 | `amount * (index[final_period] / index[current_period])`. 6 | Basically you are future-valuing the amount to the final period based on the 7 | returns of the PME. 8 | 9 | See also: 10 | - 11 | - 12 | -------------------------------------------------------------------------------- /docs/_inline/pe/ln_pme.md: -------------------------------------------------------------------------------- 1 | The basic idea of Long Nickels method is that the cash flows of a VC fund i.e. 2 | contributions and distributions are invested in a stock market index and to 3 | generate a net asset value (NAV) at the end of each period. The last NAV is 4 | used to calculate the IRR and this IRR is the Long Nickels PME. 5 | 6 | See also: 7 | - `ln_pme_nav` function 8 | - 9 | - 10 | -------------------------------------------------------------------------------- /docs/_inline/pe/ln_pme_nav.md: -------------------------------------------------------------------------------- 1 | Use the Long-Nickels method to re-calculate the private equity nav to match the 2 | public market equivalents (PME) for comparison. This method just re-calculates 3 | the nav. Instead of relying on the given nav, it is calculated as the future 4 | valued contributions less the future valued distributions. 5 | 6 | This will look like (for two periods with a contribution and distribution in each): 7 | ``` 8 | nav = c[1] * index[-1]/index[1] + c[2] * index[-1]/index[2] 9 | - d[1] * index[-1]/index[1] - d[2] * index[-1]/index[2] 10 | ``` 11 | 12 | See also: 13 | - 14 | - 15 | -------------------------------------------------------------------------------- /docs/_inline/pe/m_pme.md: -------------------------------------------------------------------------------- 1 | mPME is similar to PME+ in the sense that it uses a scaling factor. However, 2 | mPME uses different scaling factors for cash flows at different time intervals. 3 | Thus, it attempts to improve the limitations of PME+ where a single coefficient 4 | λ is used to scale all distributions. 5 | 6 | See also: 7 | - 8 | - 9 | -------------------------------------------------------------------------------- /docs/_inline/pe/moic.md: -------------------------------------------------------------------------------- 1 | The Multiple on Invested Capital (MOIC) measures the performance of an 2 | investment today relative to the initial investment. 3 | 4 | Formula: (Realized investment + Unrealized investment) / Initial Investment 5 | 6 | Unrealised value, also referred to as residual value (or NAV), is the total 7 | value of the remaining portfolio’s active investments that have not yet been 8 | liquidated. 9 | 10 | See also: 11 | - 12 | - 13 | -------------------------------------------------------------------------------- /docs/_inline/pe/pme_plus.md: -------------------------------------------------------------------------------- 1 | PME+ discount every distribution by a factor computed so that the NAV of the 2 | index investment matches the NAV of the fund. The PME+ returns an IRR value of 3 | discounted distributions. 4 | 5 | See also: 6 | - `pme_plus_flows` and `pme_plus_lambda` functions 7 | - 8 | - 9 | -------------------------------------------------------------------------------- /docs/_inline/pe/pme_plus_flows.md: -------------------------------------------------------------------------------- 1 | Use the PME+ method to re-scale the private equity flows to match the public 2 | market equivalents (PME) for comparison. This method works as follows: create 3 | an equation that sets the NAV equal to the contributions future valued based on 4 | the PME returns, minus the distributions multiplied by a scalar (lambda) future 5 | valued based on the PME returns. 6 | 7 | This will look like (for two periods with a contribution and distribution in each): 8 | ``` 9 | nav = c[1] * index[-1]/index[1] + c[2] * index[-1]/index[2] 10 | - d[1] * λ * index[-1]/index[1] - d[2] * λ * index[-1]/index[2] 11 | ``` 12 | Solve for lambda so that the two sides of the equation are equal. Then multiply 13 | all the distributions by lambda to re-scale them. 14 | 15 | See also: 16 | - 17 | - 18 | -------------------------------------------------------------------------------- /docs/_inline/pe/pme_plus_lambda.md: -------------------------------------------------------------------------------- 1 | Find λ used in PME+ method. 2 | 3 | Formula: λ = (Scaled Distributions - NAV) / Scaled Contributions 4 | Where: 5 | - Scaled Distributions = sum(distributions * index[last] / index[current]) 6 | - Scaled Contributions = sum(contributions * index[last] / index[current]) 7 | 8 | See also: 9 | - `pme_plus_flows` function 10 | - 11 | - 12 | -------------------------------------------------------------------------------- /docs/_inline/pe/rvpi.md: -------------------------------------------------------------------------------- 1 | Residual Value to Paid-In Capital (RVPI) is a term used to measure the residual 2 | value (NAV) of a private equity fund as a multiple of the capital paid in by the 3 | investors. The residual value is the current fair value of all assets held by 4 | the fund and the paid-in capital by the investors is the total of all 5 | contributed capital up to that time. 6 | 7 | Formula: Residual Value / Paid-In Capital 8 | 9 | See also: 10 | - 11 | - 12 | -------------------------------------------------------------------------------- /docs/_inline/pe/tvpi.md: -------------------------------------------------------------------------------- 1 | Total Value to Paid-In Capital (also known as the 'Investment Multiple') is a 2 | measure of the performance of a private equity fund. It represents the total 3 | value of a fund relative to the amount of capital paid into the fund to date. 4 | The total value of a fund is the sum of realised value (all distributions made 5 | to investors to date) plus the unrealised value (residual value of investments) 6 | still held by the fund. 7 | 8 | Formula: (Distributed Capital + residual Value) / Paid-In Capital 9 | 10 | See also: 11 | - 12 | - 13 | -------------------------------------------------------------------------------- /docs/bench/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Benchmarks 11 | 12 | 13 |
14 | 15 |
16 |
17 | Powered by 18 | github-action-benchmark 23 | plotly.js 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/bench/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | (function () { 3 | const data = window.BENCHMARK_DATA; 4 | // Render footer 5 | document.getElementById("download-button").onclick = () => { 6 | const dataUrl = "data:," + JSON.stringify(data, null, 2); 7 | const a = document.createElement("a"); 8 | a.href = dataUrl; 9 | a.download = "benchmark_data.json"; 10 | a.click(); 11 | }; 12 | 13 | const benches = _(data["entries"]).values().first(); 14 | const colorway = ["#ff7f0e", "#2ca02c", "#1f77b4"]; 15 | const implCodes = { rust: 1, scipy: 2, python: 3 }; 16 | 17 | const lastBenches = _.last(benches)["benches"].map((bench) => { 18 | let [_, impl, sample_size] = bench.name.split("_"); 19 | bench.impl = impl; 20 | bench.sample_size = sample_size; 21 | bench.implCode = implCodes[impl]; 22 | return bench; 23 | }); 24 | 25 | const formatRatio = (x) => { 26 | return "x" + Plotly.d3.format("0.1f")(x); 27 | }; 28 | const formatDuration = (x) => { 29 | return Plotly.d3.format("0.4s")(x.value / 1e9) + "s"; 30 | }; 31 | 32 | const cmpChartData = _(lastBenches) 33 | .orderBy(["implCode", "value"]) 34 | .groupBy("impl") 35 | .map((impl) => ({ 36 | x: _.map(impl, "sample_size"), 37 | y: _.map(impl, (x) => x.value / 1e9), // convert from ns to sec 38 | name: impl[0].impl, 39 | type: "bar", 40 | text: _.map(impl, (x, i) => { 41 | let duration = formatDuration(x); 42 | 43 | if (x.impl != "rust") { 44 | const group = _.filter(lastBenches, { 45 | impl: "rust", 46 | sample_size: x.sample_size, 47 | }); 48 | const ratio = formatRatio(x.value / group[0].value); 49 | duration += `
(${ratio})`; 50 | } 51 | return duration; 52 | }), 53 | textposition: "auto", 54 | hoverinfo: "none", 55 | })) 56 | .value(); 57 | 58 | const cmpLayout = { 59 | title: "PyXIRR vs other implementations", 60 | barmode: "group", 61 | xaxis: { 62 | title: "Sample size", 63 | type: "category", 64 | }, 65 | yaxis: { 66 | title: "Execution time", 67 | rangemode: "tozero", 68 | autorange: true, 69 | tickformat: "0.2s", 70 | ticksuffix: "s", 71 | hoverformat: ".4s", 72 | }, 73 | colorway: colorway, 74 | }; 75 | 76 | Plotly.newPlot("comparison", cmpChartData, cmpLayout); 77 | 78 | const compiled = _.template(` 79 | 80 | Implementation 81 | Sample size 82 | Execution time 83 | 84 | <% _.forEach(benches, function(bench) { %> 85 | 86 | <%- bench.impl %> 87 | <%- bench.sample_size %> 88 | <%- format(bench) %> 89 | 90 | <% }); %> 91 | `); 92 | 93 | document.getElementById("comparison-table").innerHTML = compiled({ 94 | benches: _.orderBy(lastBenches, ["sample_size", "implCode"]), 95 | format: formatDuration, 96 | }); 97 | 98 | const perfChartData = [ 99 | { 100 | y: _(benches) 101 | .map("benches") 102 | .flatten() 103 | .filter({ name: "bench_rust_100" }) 104 | .map((x) => x.value / 1e9) 105 | .value(), 106 | x: _.range(benches.length), 107 | text: _.map(benches, (x) => x.commit.id.slice(0, 7)), 108 | }, 109 | ]; 110 | 111 | const perfLayout = { 112 | title: "PyXIRR performance over time", 113 | barmode: "group", 114 | xaxis: { 115 | title: "Commit", 116 | ticktext: perfChartData[0].text, 117 | tickvals: perfChartData[0].x, 118 | }, 119 | yaxis: { 120 | title: "Execution time", 121 | rangemode: "tozero", 122 | autorange: true, 123 | tickformat: ".2s", 124 | hoverformat: ".4s", 125 | ticksuffix: "s", 126 | }, 127 | colorway: colorway, 128 | }; 129 | 130 | Plotly.newPlot("performance", perfChartData, perfLayout); 131 | })(); 132 | -------------------------------------------------------------------------------- /docs/bench/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", Roboto, Oxygen, 3 | Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, 4 | Arial, sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | background-color: #fff; 7 | font-size: 16px; 8 | } 9 | body { 10 | color: #4a4a4a; 11 | margin: 8px; 12 | font-size: 1em; 13 | font-weight: 400; 14 | } 15 | main { 16 | width: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | a { 22 | color: #3273dc; 23 | cursor: pointer; 24 | text-decoration: none; 25 | } 26 | a:hover { 27 | color: #000; 28 | } 29 | button { 30 | color: #fff; 31 | background-color: #3298dc; 32 | border-color: transparent; 33 | cursor: pointer; 34 | text-align: center; 35 | } 36 | button:hover { 37 | background-color: #2793da; 38 | flex: none; 39 | } 40 | .spacer { 41 | flex: auto; 42 | } 43 | .small { 44 | font-size: 0.75rem; 45 | } 46 | header { 47 | margin-top: 16px; 48 | display: flex; 49 | align-items: center; 50 | } 51 | .header-label { 52 | margin-right: 4px; 53 | } 54 | .benchmark-set { 55 | margin: 8px 0; 56 | width: 100%; 57 | display: flex; 58 | flex-direction: column; 59 | } 60 | .benchmark-title { 61 | font-size: 3rem; 62 | font-weight: 600; 63 | word-break: break-word; 64 | text-align: center; 65 | } 66 | .benchmark-graphs { 67 | display: flex; 68 | flex-direction: row; 69 | justify-content: space-around; 70 | align-items: center; 71 | flex-wrap: wrap; 72 | width: 100%; 73 | } 74 | .benchmark-chart { 75 | max-width: 1000px; 76 | } 77 | 78 | .benchmark-table { 79 | line-height: 1.5; 80 | color: #000 !important; 81 | box-sizing: inherit; 82 | font-size: 15px; 83 | margin-top: 20px; 84 | margin-bottom: 40px; 85 | border-collapse: collapse; 86 | width: 500px; 87 | padding: 8px; 88 | text-align: left; 89 | border-bottom: 1px solid #ddd; 90 | } 91 | -------------------------------------------------------------------------------- /docs/bench/vectorization/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 13 | 14 | Vectorization benchmark 15 | 22 | 23 | 24 |

Imports

25 |
 26 |         
 27 | import numpy as np
 28 | import numpy_financial as npf
 29 | import perfplot
 30 | import pyxirr
 31 |         
 32 |     
33 |

Case 1

34 |

35 | Input: numpy array
36 | Output: numpy array
37 |

38 |
 39 |         
 40 | perfplot.show(
 41 |     setup=lambda n: np.random.rand(n) / 12,
 42 |     kernels=[
 43 |         lambda a: pyxirr.fv(a, 120, -100, -100),
 44 |         lambda a: npf.fv(a, 120, -100, -100),
 45 |     ],
 46 |     labels=["pyxirr", "npf"],
 47 |     n_range=[2**k for k in range(1, 20)],
 48 |     xlabel="len(a)",
 49 |     equality_check=np.allclose,
 50 | )
 51 |         
 52 |     
53 | 54 |

Case 2

55 |

56 | Input: list
57 | Output: list
58 |

59 |
 60 |         
 61 | perfplot.show(
 62 |     setup=lambda n: (np.random.rand(n) / 12).tolist(),
 63 |     kernels=[
 64 |         lambda a: pyxirr.fv(a, 120, -100, -100),
 65 |         lambda a: npf.fv(a, 120, -100, -100).tolist(),
 66 |     ],
 67 |     labels=["pyxirr", "npf"],
 68 |     n_range=[2**k for k in range(1, 20)],
 69 |     xlabel="len(a)",
 70 |     equality_check=np.allclose,
 71 | )
 72 |         
 73 |     
74 | 75 |

Case 3

76 |

77 | Input: list
78 | Output: list (pyxirr), numpy array (npf)
79 |

80 |
 81 |         
 82 | perfplot.show(
 83 |     setup=lambda n: (np.random.rand(n) / 12).tolist(),
 84 |     kernels=[
 85 |         lambda a: pyxirr.fv(a, 120, -100, -100),
 86 |         lambda a: npf.fv(a, 120, -100, -100),
 87 |     ],
 88 |     labels=["pyxirr", "npf"],
 89 |     n_range=[2**k for k in range(1, 20)],
 90 |     xlabel="len(a)",
 91 |     equality_check=np.allclose,
 92 | )
 93 |         
 94 |     
95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /docs/bench/vectorization/list-in-default-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anexen/pyxirr/f012d1499f449024ad92b42a26f571d58ba330b2/docs/bench/vectorization/list-in-default-out.png -------------------------------------------------------------------------------- /docs/bench/vectorization/list-in-list-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anexen/pyxirr/f012d1499f449024ad92b42a26f571d58ba330b2/docs/bench/vectorization/list-in-list-out.png -------------------------------------------------------------------------------- /docs/bench/vectorization/ndarray-in-default-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anexen/pyxirr/f012d1499f449024ad92b42a26f571d58ba330b2/docs/bench/vectorization/ndarray-in-default-out.png -------------------------------------------------------------------------------- /docs/bench/vectorization/ndarray-in-ndarray-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anexen/pyxirr/f012d1499f449024ad92b42a26f571d58ba330b2/docs/bench/vectorization/ndarray-in-ndarray-out.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # PyXIRR 2 | 3 | Rust-powered collection of financial functions. 4 | 5 | # Table of Contents 6 | 7 | - [Installation](install.md) 8 | - [Functions](functions.md) 9 | - [Type annotations](functions.md#type-annotations) 10 | - [Day Count Conventions](functions.md#day-count-conventions) 11 | - [Exceptions](functions.md#exceptions) 12 | - [FV](functions.md#fv) 13 | - [NFV](functions.md#nfv) 14 | - [XFV](functions.md#xfv) 15 | - [XNFV](functions.md#xnfv) 16 | - [PMT](functions.md#pmt) 17 | - [IPMT](functions.md#ipmt) 18 | - [PPMT](functions.md#ppmt) 19 | - [NPER](functions.md#nper) 20 | - [RATE](functions.md#rate) 21 | - [PV](functions.md#pv) 22 | - [NPV](functions.md#npv) 23 | - [XNPV](functions.md#xnpv) 24 | - [IRR](functions.md#irr) 25 | - [MIRR](functions.md#mirr) 26 | - [XIRR](functions.md#xirr) 27 | - [Private Equity](private_equity.md) 28 | - [DPI](private_equity.md#dpi) 29 | - [RVPI](private_equity.md#rvpi) 30 | - [TVPI](private_equity.md#tvpi) 31 | - [MOIC](private_equity.md#moic) 32 | - [LN-PME NAV](private_equity.md#ln_pme_nav) 33 | - [LN-PME](private_equity.md#ln_pme) 34 | - [KS-PME Flows](private_equity.md#ks_pme_flows) 35 | - [KS-PME](private_equity.md#ks_pme) 36 | - [mPME](private_equity.md#m_pme) 37 | - [PME+ Flows](private_equity.md#pme_plus_flows) 38 | - [PME+ Lambda](private_equity.md#pme_plus_lambda) 39 | - [PME+](private_equity.md#pme_plus) 40 | - [Direct Alpha](private_equity.md#direct_alpha) 41 | - [Benchmarks](bench/index.html) 42 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | > Supported python versions depend on [pyo3](https://github.com/PyO3/pyo3) which supports Python 3.7 and up. 2 | 3 | # Installation 4 | 5 | PyXIRR doesn't have external dependencies. 6 | 7 | ```bash 8 | pip install pyxirr 9 | ``` 10 | 11 | ## Installation from source 12 | 13 | To install PyXIRR from source you need [Rust](https://www.rust-lang.org) version 1.48 or higher. 14 | 15 | Thanks to [PEP-517](https://www.python.org/dev/peps/pep-0517/) and [maturin](https://github.com/PyO3/maturin), installation from source as simple as 16 | 17 | ```bash 18 | pip install path/to/pyxirr 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/private_equity.md: -------------------------------------------------------------------------------- 1 | {% include head.html %} 2 | 3 | ## Type annotations 4 | 5 | ```python 6 | Amount = Union[int, float, Decimal] # also supports numpy types 7 | AmountArray = Iterable[Amount] 8 | ``` 9 | 10 | ## DPI 11 | 12 | ```python 13 | def dpi(amounts: AmountArray) -> float: 14 | ... 15 | 16 | 17 | def dpi_2( 18 | contributions: AmountArray, 19 | distributions: AmountArray, 20 | ) -> float: 21 | ... 22 | ``` 23 | 24 | {% include_relative _inline/pe/dpi.md %} 25 | 26 | ## RVPI 27 | 28 | ```python 29 | def rvpi( 30 | contributions: AmountArray, 31 | nav: Amount, 32 | ) -> float: 33 | ... 34 | ``` 35 | 36 | {% include_relative _inline/pe/rvpi.md %} 37 | 38 | ## TVPI 39 | 40 | ```python 41 | def tvpi( 42 | amounts: AmountArray, 43 | nav: Amount = 0, 44 | ) -> float: 45 | ... 46 | 47 | 48 | def tvpi_2( 49 | contributions: AmountArray, 50 | distributions: AmountArray, 51 | nav: Amount = 0, 52 | ) -> float: 53 | ... 54 | ``` 55 | 56 | {% include_relative _inline/pe/tvpi.md %} 57 | 58 | ## MOIC 59 | 60 | ```python 61 | def moic( 62 | amounts: AmountArray, 63 | nav: Amount = 0, 64 | ) -> float: 65 | ... 66 | 67 | 68 | def moic_2( 69 | contributions: AmountArray, 70 | distributions: AmountArray, 71 | nav: Amount = 0, 72 | ) -> float: 73 | ... 74 | ``` 75 | 76 | {% include_relative _inline/pe/moic.md %} 77 | 78 | ## LN-PME 79 | 80 | ```python 81 | def ln_pme( 82 | amounts: AmountArray, 83 | index: AmountArray, 84 | ) -> Optional[float]: 85 | ... 86 | 87 | 88 | def ln_pme_2( 89 | contributions: AmountArray, 90 | distributions: AmountArray, 91 | index: AmountArray, 92 | ) -> Optional[float]: 93 | ... 94 | ``` 95 | 96 | {% include_relative _inline/pe/ln_pme.md %} 97 | 98 | ## LN-PME NAV 99 | 100 | ```python 101 | def ln_pme_nav( 102 | amounts: AmountArray, 103 | index: AmountArray, 104 | ) -> float: 105 | ... 106 | 107 | 108 | def ln_pme_nav_2( 109 | contributions: AmountArray, 110 | distributions: AmountArray, 111 | index: AmountArray, 112 | ) -> float: 113 | ... 114 | ``` 115 | 116 | {% include_relative _inline/pe/ln_pme_nav.md %} 117 | 118 | ## KS-PME Flows 119 | 120 | ```python 121 | def ks_pme_flows( 122 | amounts: AmountArray, 123 | index: AmountArray, 124 | ) -> List[float]: 125 | ... 126 | 127 | 128 | def ks_pme_flows_2( 129 | contributions: AmountArray, 130 | distributions: AmountArray, 131 | index: AmountArray, 132 | ) -> Tuple[List[float], List[float]]: 133 | ... 134 | ``` 135 | 136 | {% include_relative _inline/pe/ks_pme_flows.md %} 137 | 138 | ## KS-PME 139 | 140 | ```python 141 | def ks_pme( 142 | amounts: AmountArray, 143 | index: AmountArray, 144 | nav: Amount = 0, 145 | ) -> Optional[float]: 146 | ... 147 | 148 | 149 | def ks_pme_2( 150 | contributions: AmountArray, 151 | distributions: AmountArray, 152 | index: AmountArray, 153 | nav: Amount = 0, 154 | ) -> Optional[float]: 155 | ... 156 | ``` 157 | 158 | {% include_relative _inline/pe/ks_pme.md %} 159 | 160 | ## mPME 161 | 162 | ```python 163 | def m_pme( 164 | amounts: AmountArray, 165 | index: AmountArray, 166 | nav: AmountArray, 167 | ) -> float: 168 | ... 169 | 170 | 171 | def m_pme_2( 172 | contributions: AmountArray, 173 | distributions: AmountArray, 174 | index: AmountArray, 175 | nav: AmountArray, 176 | ) -> float: 177 | ... 178 | ``` 179 | 180 | {% include_relative _inline/pe/m_pme.md %} 181 | 182 | ## PME+ Flows 183 | 184 | ```python 185 | def pme_plus_flows( 186 | amounts: AmountArray, 187 | index: AmountArray, 188 | nav: Amount = 0, 189 | ) -> List[float]: 190 | ... 191 | 192 | 193 | def pme_plus_flows_2( 194 | contributions: AmountArray, 195 | distributions: AmountArray, 196 | index: AmountArray, 197 | nav: Amount = 0, 198 | ) -> Tuple[List[float], List[float]]: 199 | ... 200 | ``` 201 | 202 | {% include_relative _inline/pe/pme_plus_flows.md %} 203 | 204 | ## PME+ Lambda 205 | 206 | ```python 207 | def pme_plus_lambda( 208 | amounts: AmountArray, 209 | index: AmountArray, 210 | nav: Amount = 0, 211 | ) -> float: 212 | ... 213 | 214 | 215 | def pme_plus_lambda_2( 216 | contributions: AmountArray, 217 | distributions: AmountArray, 218 | index: AmountArray, 219 | nav: Amount = 0, 220 | ) -> float: 221 | ... 222 | ``` 223 | 224 | {% include_relative _inline/pe/pme_plus_lambda.md %} 225 | 226 | ## PME+ 227 | 228 | ```python 229 | def pme_plus( 230 | amounts: AmountArray, 231 | index: AmountArray, 232 | nav: Amount = 0, 233 | ) -> Optional[float]: 234 | ... 235 | 236 | 237 | def pme_plus_2( 238 | contributions: AmountArray, 239 | distributions: AmountArray, 240 | index: AmountArray, 241 | nav: Amount = 0, 242 | ) -> Optional[float]: 243 | ... 244 | ``` 245 | 246 | {% include_relative _inline/pe/pme_plus.md %} 247 | 248 | ## Direct Alpha 249 | 250 | ```python 251 | def direct_alpha( 252 | amounts: AmountArray, 253 | index: AmountArray, 254 | nav: Amount = 0, 255 | ) -> Optional[float]: 256 | ... 257 | 258 | 259 | def direct_alpha_2( 260 | contributions: AmountArray, 261 | distributions: AmountArray, 262 | index: AmountArray, 263 | nav: Amount = 0, 264 | ) -> Optional[float]: 265 | ... 266 | ``` 267 | 268 | {% include_relative _inline/pe/direct_alpha.md %} 269 | -------------------------------------------------------------------------------- /docs/static/bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anexen/pyxirr/f012d1499f449024ad92b42a26f571d58ba330b2/docs/static/bench.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pyxirr" 3 | description-content-type = "text/markdown; charset=UTF-8; variant=GFM" 4 | requires-python = ">=3.7,<3.14" 5 | dynamic = ["version"] 6 | classifiers = [ 7 | "Development Status :: 4 - Beta", 8 | "Topic :: Office/Business :: Financial", 9 | "Programming Language :: Rust", 10 | "Programming Language :: Python :: Implementation :: CPython", 11 | "Programming Language :: Python :: 3.7", 12 | "Programming Language :: Python :: 3.8", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Programming Language :: Python :: 3.13", 18 | "License :: OSI Approved :: The Unlicense (Unlicense)", 19 | ] 20 | 21 | [build-system] 22 | requires = ["maturin>=1,<2"] 23 | build-backend = "maturin" 24 | 25 | [tool.maturin] 26 | python-source = "python" 27 | module-name = "pyxirr._pyxirr" 28 | features = ["pyo3/extension-module"] 29 | strip = true 30 | include = [ 31 | { path = "pyproject.toml", format = ["sdist", "wheel"] }, 32 | { path = "Cargo.toml", format = "sdist" }, 33 | { path = "Cargo.lock", format = "sdist" }, 34 | { path = ".cargo/*", format = "sdist" }, 35 | { path = "docs", format = "sdist" }, 36 | ] 37 | 38 | [tool.black] 39 | line-length = 79 40 | target-version = ["py311"] 41 | 42 | [tool.isort] 43 | profile = "black" 44 | atomic = true 45 | line_length = 79 46 | lines_after_imports = 2 47 | -------------------------------------------------------------------------------- /python/pyxirr/__init__.py: -------------------------------------------------------------------------------- 1 | from ._pyxirr import * 2 | 3 | __doc__ = _pyxirr.__doc__ 4 | __all__ = _pyxirr.__all__ 5 | -------------------------------------------------------------------------------- /python/pyxirr/_pyxirr.pyi: -------------------------------------------------------------------------------- 1 | import sys 2 | from collections.abc import Iterable, Sequence 3 | from datetime import date, datetime 4 | from decimal import Decimal 5 | from typing import ( 6 | Any, 7 | Dict, 8 | Hashable, 9 | List, 10 | Optional, 11 | Tuple, 12 | TypeVar, 13 | Union, 14 | overload, 15 | ) 16 | 17 | 18 | if sys.version_info >= (3, 8): 19 | from typing import Literal, Protocol 20 | else: 21 | from typing_extensions import Literal, Protocol 22 | 23 | # We are using protocols because mypy does not support dynamic type hints for 24 | # optional libraries, e.g in the ideal world: 25 | # _DateLike = Union[date, datetime] if pandas is not installed 26 | # _DateLike = Union[date, datetime, pandas.Timestamp] if pandas is installed 27 | # but it is impossible to archive. 28 | 29 | # fmt: off 30 | 31 | _Shape = Tuple[int, ...] 32 | _OrderKACF = Optional[Literal["K", "A", "C", "F"]] 33 | 34 | class _ndarray(Protocol): 35 | @property 36 | def ndim(self) -> int: ... 37 | @property 38 | def size(self) -> int: ... 39 | @property 40 | def shape(self) -> _Shape: ... 41 | @property 42 | def strides(self) -> _Shape: ... 43 | @property 44 | def dtype(self) -> Any: ... 45 | def flatten( self, order: _OrderKACF = ...) -> "_ndarray": ... 46 | def ravel( self, order: _OrderKACF = ...,) -> "_ndarray": ... 47 | def fill(self, value: Any) -> None: ... 48 | 49 | class _DatetimeScalar(Protocol): 50 | @property 51 | def day(self) -> int: ... 52 | @property 53 | def month(self) -> int: ... 54 | @property 55 | def year(self) -> int: ... 56 | 57 | class _datetime64(Protocol): 58 | def __init__( 59 | self, 60 | value: Union[None, int , str, "_datetime64", _DatetimeScalar] = ..., 61 | format: Union[str, Tuple[str, int]] = ..., 62 | ) -> None: ... 63 | 64 | # define some specific methods just to recognize Series/DataFrame 65 | # https://github.com/VirtusLab/pandas-stubs 66 | 67 | _Label = Optional[Hashable] 68 | 69 | class _Series(Protocol): 70 | @property 71 | def hasnans(self) -> bool: ... 72 | def items(self) -> Iterable[Any]: ... 73 | def iteritems(self) -> Iterable[Any]: ... 74 | def to_frame(self, name: Optional[Any] = ...) -> "_DataFrame": ... 75 | 76 | class _DataFrame(Protocol): 77 | @property 78 | def shape(self) -> Tuple[int, int]: ... 79 | def items(self) -> Iterable[Tuple[_Label, _Series]]: ... 80 | def iteritems(self) -> Iterable[Tuple[_Label, _Series]]: ... 81 | def iterrows(self) -> Iterable[Tuple[_Label, _Series]]: ... 82 | def itertuples( self, index: bool = ..., name: Optional[str] = ...) -> Iterable[Tuple[Any, ...]]: ... 83 | def assign(self, **kwargs: Any) -> "_DataFrame": ... 84 | 85 | class _Timestamp(Protocol): 86 | def isoformat(self, sep: str = ...) -> str: ... 87 | def day_name(self, locale: Optional[str]) -> str: ... 88 | def month_name(self, locale: Optional[str]) -> str: ... 89 | def normalize(self) -> "_Timestamp": ... 90 | @property 91 | def is_month_end(self) -> bool: ... 92 | @property 93 | def is_month_start(self) -> bool: ... 94 | @property 95 | def is_quarter_start(self) -> bool: ... 96 | @property 97 | def is_quarter_end(self) -> bool: ... 98 | @property 99 | def is_year_start(self) -> bool: ... 100 | @property 101 | def is_year_end(self) -> bool: ... 102 | @property 103 | def is_leap_year(self) -> bool: ... 104 | 105 | # fmt: on 106 | 107 | 108 | # rate as decimal, not percentage, normally between [-1, 1] 109 | _Rate = Union[float, Decimal] 110 | _Period = Union[int, float, Decimal] 111 | _Guess = Optional[_Rate] 112 | _Amount = Union[int, float, Decimal] 113 | _DayCount = Union["DayCount" | str] 114 | 115 | _DateLike = Union[str, date, datetime, _datetime64, _Timestamp] 116 | _Payment = Tuple[_DateLike, _Amount] 117 | _CashFlowTable = Union[Iterable[_Payment], _DataFrame, _ndarray] 118 | _CashFlowDict = Dict[_DateLike, _Amount] 119 | _CashFlow = Union[_CashFlowTable, _CashFlowDict, _Series] 120 | 121 | _DateLikeArray = Iterable[_DateLike] 122 | _AmountArray = Iterable[_Amount] 123 | 124 | _T = TypeVar("_T") 125 | _ArrayLike = Union[ 126 | _ndarray, 127 | Sequence[_T], 128 | Sequence[Sequence[_T]], 129 | Sequence[Sequence[Sequence[_T]]], 130 | Sequence[Sequence[Sequence[Sequence[_T]]]], 131 | Sequence[Sequence[Sequence[Sequence[Sequence[_T]]]]], 132 | Sequence[Sequence[Sequence[Sequence[Sequence[Sequence[_T]]]]]], 133 | Sequence[Sequence[Sequence[Sequence[Sequence[Sequence[Sequence[_T]]]]]]], 134 | Sequence[ 135 | Sequence[ 136 | Sequence[Sequence[Sequence[Sequence[Sequence[Sequence[_T]]]]]] 137 | ] 138 | ], 139 | ] 140 | _ScalarOrArrayLike = Union[_T, _ArrayLike[_T]] 141 | 142 | 143 | class InvalidPaymentsError(Exception): 144 | pass 145 | 146 | 147 | class BroadcastingError(Exception): 148 | pass 149 | 150 | 151 | class DayCount: 152 | ACT_ACT_ISDA: "DayCount" 153 | ACT_365F: "DayCount" 154 | ACT_365_25: "DayCount" 155 | ACT_364: "DayCount" 156 | ACT_360: "DayCount" 157 | THIRTY_360_ISDA: "DayCount" 158 | THIRTY_E_360: "DayCount" 159 | THIRTY_E_PLUS_360: "DayCount" 160 | THIRTY_E_360_ISDA: "DayCount" 161 | THIRTY_U_360: "DayCount" 162 | NL_365: "DayCount" 163 | NL_360: "DayCount" 164 | 165 | @staticmethod 166 | def of(day_count: str) -> "DayCount": 167 | ... 168 | 169 | 170 | def year_fraction(d1: _DateLike, d2: _DateLike, day_count: _DayCount) -> float: 171 | ... 172 | 173 | 174 | def days_between(d1: _DateLike, d2: _DateLike, day_count: _DayCount) -> int: 175 | ... 176 | 177 | 178 | @overload 179 | def fv( # type: ignore[misc] 180 | rate: _Rate, 181 | nper: _Period, 182 | pmt: _Amount, 183 | pv: _Amount, 184 | *, 185 | pmt_at_beginning: bool = False, 186 | ) -> Optional[float]: 187 | ... 188 | 189 | 190 | @overload 191 | def fv( 192 | rate: _ScalarOrArrayLike[_Rate], 193 | nper: _ScalarOrArrayLike[_Period], 194 | pmt: _ScalarOrArrayLike[_Amount], 195 | pv: _ScalarOrArrayLike[_Amount], 196 | *, 197 | pmt_at_beginning: _ScalarOrArrayLike[bool] = False, 198 | ) -> List[Optional[float]]: 199 | ... 200 | 201 | 202 | def nfv( 203 | rate: _Rate, # Rate of interest per period 204 | nper: _Period, # Number of compounding periods 205 | amounts: _AmountArray, 206 | ) -> Optional[float]: 207 | ... 208 | 209 | 210 | def xfv( 211 | start_date: _DateLike, 212 | cash_flow_date: _DateLike, 213 | end_date: _DateLike, 214 | cash_flow_rate: _Rate, # annual rate 215 | end_rate: _Rate, # annual rate 216 | cash_flow: _Amount, 217 | ) -> Optional[float]: 218 | ... 219 | 220 | 221 | @overload 222 | def xnfv( 223 | rate: _Rate, # annual rate 224 | dates: _CashFlow, 225 | *, 226 | silent: bool = False, 227 | day_count: _DayCount = DayCount.ACT_365F, 228 | ) -> Optional[float]: 229 | ... 230 | 231 | 232 | @overload 233 | def xnfv( 234 | rate: _Rate, # annual rate 235 | dates: _DateLikeArray, 236 | amounts: _AmountArray, 237 | *, 238 | silent: bool = False, 239 | day_count: _DayCount = DayCount.ACT_365F, 240 | ) -> Optional[float]: 241 | ... 242 | 243 | 244 | @overload 245 | def pv( # type: ignore[misc] 246 | rate: _Rate, 247 | nper: _Period, 248 | pmt: _Amount, 249 | fv: _Amount, 250 | *, 251 | pmt_at_beginning: bool = False, 252 | ) -> Optional[float]: 253 | ... 254 | 255 | 256 | @overload 257 | def pv( 258 | rate: _ScalarOrArrayLike[_Rate], 259 | nper: _ScalarOrArrayLike[_Period], 260 | pmt: _ScalarOrArrayLike[_Amount], 261 | fv: _ScalarOrArrayLike[_Amount], 262 | *, 263 | pmt_at_beginning: _ScalarOrArrayLike[bool] = False, 264 | ) -> List[Optional[float]]: 265 | ... 266 | 267 | 268 | @overload 269 | def npv( 270 | rate: _Rate, 271 | amounts: _AmountArray, 272 | *, 273 | start_from_zero: bool = True, 274 | ) -> Optional[float]: 275 | ... 276 | 277 | 278 | @overload 279 | def npv( 280 | rate: Iterable[_Rate], 281 | amounts: _AmountArray, 282 | *, 283 | start_from_zero: bool = True, 284 | ) -> List[Optional[float]]: 285 | ... 286 | 287 | 288 | @overload 289 | def xnpv( 290 | rate: _Rate, 291 | dates: _CashFlow, 292 | *, 293 | silent: bool = False, 294 | day_count: _DayCount = DayCount.ACT_365F, 295 | ) -> Optional[float]: 296 | ... 297 | 298 | 299 | @overload 300 | def xnpv( 301 | rate: _Rate, 302 | dates: _DateLikeArray, 303 | amounts: _AmountArray, 304 | *, 305 | silent: bool = False, 306 | day_count: _DayCount = DayCount.ACT_365F, 307 | ) -> Optional[float]: 308 | ... 309 | 310 | 311 | @overload 312 | def xnpv( 313 | rate: Iterable[_Rate], 314 | dates: _CashFlow, 315 | *, 316 | silent: bool = False, 317 | day_count: _DayCount = DayCount.ACT_365F, 318 | ) -> List[Optional[float]]: 319 | ... 320 | 321 | 322 | @overload 323 | def rate( # type: ignore[misc] 324 | nper: _Period, 325 | pmt: _Amount, 326 | pv: _Amount, 327 | fv: _Amount = 0, 328 | *, 329 | pmt_at_beginning: bool = False, 330 | guess: _Guess = None, 331 | ) -> Optional[float]: 332 | ... 333 | 334 | 335 | @overload 336 | def rate( 337 | nper: _ScalarOrArrayLike[_Period], 338 | pmt: _ScalarOrArrayLike[_Amount], 339 | pv: _ScalarOrArrayLike[_Amount], 340 | fv: _ScalarOrArrayLike[_Amount] = 0, 341 | *, 342 | pmt_at_beginning: _ScalarOrArrayLike[bool] = False, 343 | guess: _Guess = None, 344 | ) -> List[Optional[float]]: 345 | ... 346 | 347 | 348 | @overload 349 | def nper( # type: ignore[misc] 350 | rate: _Rate, 351 | pmt: _Amount, 352 | pv: _Amount, 353 | fv: _Amount = 0, 354 | *, 355 | pmt_at_beginning: bool = False, 356 | ) -> Optional[float]: 357 | ... 358 | 359 | 360 | @overload 361 | def nper( 362 | rate: _ScalarOrArrayLike[_Rate], 363 | pmt: _ScalarOrArrayLike[_Amount], 364 | pv: _ScalarOrArrayLike[_Amount], 365 | fv: _ScalarOrArrayLike[_Amount] = 0, 366 | *, 367 | pmt_at_beginning: _ScalarOrArrayLike[bool] = False, 368 | ) -> List[Optional[float]]: 369 | ... 370 | 371 | 372 | @overload 373 | def pmt( # type: ignore[misc] 374 | rate: _Rate, 375 | nper: _Period, 376 | pv: _Amount, 377 | fv: _Amount = 0, 378 | *, 379 | pmt_at_beginning: bool = False, 380 | ) -> Optional[float]: 381 | ... 382 | 383 | 384 | @overload 385 | def pmt( 386 | rate: _ScalarOrArrayLike[_Rate], 387 | nper: _ScalarOrArrayLike[_Period], 388 | pv: _ScalarOrArrayLike[_Amount], 389 | fv: _ScalarOrArrayLike[_Amount] = 0, 390 | *, 391 | pmt_at_beginning: _ScalarOrArrayLike[bool] = False, 392 | ) -> List[Optional[float]]: 393 | ... 394 | 395 | 396 | @overload 397 | def ipmt( # type: ignore[misc] 398 | rate: _Rate, 399 | per: _Period, 400 | nper: _Period, 401 | pv: _Amount, 402 | fv: _Amount = 0, 403 | *, 404 | pmt_at_beginning: bool = False, 405 | ) -> Optional[float]: 406 | ... 407 | 408 | 409 | @overload 410 | def ipmt( 411 | rate: _ScalarOrArrayLike[_Rate], 412 | per: _ScalarOrArrayLike[_Period], 413 | nper: _ScalarOrArrayLike[_Period], 414 | pv: _ScalarOrArrayLike[_Amount], 415 | fv: _ScalarOrArrayLike[_Amount] = 0, 416 | *, 417 | pmt_at_beginning: _ScalarOrArrayLike[bool] = False, 418 | ) -> List[Optional[float]]: 419 | ... 420 | 421 | 422 | def cumipmt( 423 | rate: _Rate, 424 | nper: _Period, 425 | pv: _Amount, 426 | start_period: _Period, 427 | end_period: _Period, 428 | *, 429 | pmt_at_beginning: bool = False, 430 | ) -> Optional[float]: 431 | ... 432 | 433 | 434 | @overload 435 | def ppmt( # type: ignore[misc] 436 | rate: _Rate, 437 | per: _Period, 438 | nper: _Period, 439 | pv: _Amount, 440 | fv: _Amount = 0, 441 | *, 442 | pmt_at_beginning: bool = False, 443 | ) -> Optional[float]: 444 | ... 445 | 446 | 447 | @overload 448 | def ppmt( 449 | rate: _ScalarOrArrayLike[_Rate], 450 | per: _ScalarOrArrayLike[_Period], 451 | nper: _ScalarOrArrayLike[_Period], 452 | pv: _ScalarOrArrayLike[_Amount], 453 | fv: _ScalarOrArrayLike[_Amount] = 0, 454 | *, 455 | pmt_at_beginning: _ScalarOrArrayLike[bool] = False, 456 | ) -> List[Optional[float]]: 457 | ... 458 | 459 | 460 | def cumprinc( 461 | rate: _Rate, 462 | nper: _Period, 463 | pv: _Amount, 464 | start_period: _Period, 465 | end_period: _Period, 466 | *, 467 | pmt_at_beginning: bool = False, 468 | ) -> Optional[float]: 469 | ... 470 | 471 | 472 | def irr( 473 | amounts: _AmountArray, 474 | *, 475 | guess: _Guess = None, 476 | silent: bool = False, 477 | ) -> Optional[float]: 478 | ... 479 | 480 | 481 | def mirr( 482 | amounts: _AmountArray, 483 | finance_rate: _Rate, 484 | reinvest_rate: _Rate, 485 | *, 486 | silent: bool = False, 487 | ) -> Optional[float]: 488 | ... 489 | 490 | 491 | @overload 492 | def xirr( 493 | dates: _DateLikeArray, 494 | amounts: _AmountArray, 495 | *, 496 | guess: _Guess = None, 497 | silent: bool = False, 498 | day_count: _DayCount = DayCount.ACT_365F, 499 | ) -> Optional[float]: 500 | ... 501 | 502 | 503 | @overload 504 | def xirr( 505 | dates: _CashFlow, 506 | *, 507 | guess: _Guess = None, 508 | silent: bool = False, 509 | day_count: _DayCount = DayCount.ACT_365F, 510 | ) -> Optional[float]: 511 | ... 512 | 513 | 514 | def is_conventional_cash_flow(cf: _AmountArray) -> bool: 515 | ... 516 | 517 | 518 | def zero_crossing_points(cf: _AmountArray) -> list[int]: 519 | ... 520 | -------------------------------------------------------------------------------- /python/pyxirr/pe.pyi: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from decimal import Decimal 3 | from typing import List, Optional, Tuple, Union 4 | 5 | _Amount = Union[int, float, Decimal] 6 | _AmountArray = Iterable[_Amount] 7 | 8 | 9 | def dpi(amounts: _AmountArray) -> float: 10 | ... 11 | 12 | 13 | def dpi_2( 14 | contributions: _AmountArray, 15 | distributions: _AmountArray, 16 | ) -> float: 17 | ... 18 | 19 | 20 | def rvpi( 21 | contributions: _AmountArray, 22 | nav: _Amount, 23 | ) -> float: 24 | ... 25 | 26 | 27 | def tvpi( 28 | amounts: _AmountArray, 29 | nav: _Amount = 0, 30 | ) -> float: 31 | ... 32 | 33 | 34 | def tvpi_2( 35 | contributions: _AmountArray, 36 | distributions: _AmountArray, 37 | nav: _Amount = 0, 38 | ) -> float: 39 | ... 40 | 41 | 42 | def moic( 43 | amounts: _AmountArray, 44 | nav: _Amount = 0, 45 | ) -> float: 46 | ... 47 | 48 | 49 | def moic_2( 50 | contributions: _AmountArray, 51 | distributions: _AmountArray, 52 | nav: _Amount = 0, 53 | ) -> float: 54 | ... 55 | 56 | 57 | def ks_pme( 58 | amounts: _AmountArray, 59 | index: _AmountArray, 60 | nav: _Amount = 0, 61 | ) -> Optional[float]: 62 | ... 63 | 64 | 65 | def ks_pme_2( 66 | contributions: _AmountArray, 67 | distributions: _AmountArray, 68 | index: _AmountArray, 69 | nav: _Amount = 0, 70 | ) -> Optional[float]: 71 | ... 72 | 73 | 74 | def ks_pme_flows( 75 | amounts: _AmountArray, 76 | index: _AmountArray, 77 | ) -> List[float]: 78 | ... 79 | 80 | 81 | def ks_pme_flows_2( 82 | contributions: _AmountArray, 83 | distributions: _AmountArray, 84 | index: _AmountArray, 85 | ) -> Tuple[List[float], List[float]]: 86 | ... 87 | 88 | 89 | def m_pme( 90 | amounts: _AmountArray, 91 | index: _AmountArray, 92 | nav: _AmountArray, 93 | ) -> float: 94 | ... 95 | 96 | 97 | def m_pme_2( 98 | contributions: _AmountArray, 99 | distributions: _AmountArray, 100 | index: _AmountArray, 101 | nav: _AmountArray, 102 | ) -> float: 103 | ... 104 | 105 | 106 | def pme_plus( 107 | amounts: _AmountArray, 108 | index: _AmountArray, 109 | nav: _Amount = 0, 110 | ) -> Optional[float]: 111 | ... 112 | 113 | 114 | def pme_plus_2( 115 | contributions: _AmountArray, 116 | distributions: _AmountArray, 117 | index: _AmountArray, 118 | nav: _Amount = 0, 119 | ) -> Optional[float]: 120 | ... 121 | 122 | 123 | def pme_plus_flows( 124 | amounts: _AmountArray, 125 | index: _AmountArray, 126 | nav: _Amount = 0, 127 | ) -> List[float]: 128 | ... 129 | 130 | 131 | def pme_plus_flows_2( 132 | contributions: _AmountArray, 133 | distributions: _AmountArray, 134 | index: _AmountArray, 135 | nav: _Amount = 0, 136 | ) -> Tuple[List[float], List[float]]: 137 | ... 138 | 139 | 140 | def pme_plus_lambda( 141 | amounts: _AmountArray, 142 | index: _AmountArray, 143 | nav: _Amount = 0, 144 | ) -> float: 145 | ... 146 | 147 | 148 | def pme_plus_lambda_2( 149 | contributions: _AmountArray, 150 | distributions: _AmountArray, 151 | index: _AmountArray, 152 | nav: _Amount = 0, 153 | ) -> float: 154 | ... 155 | 156 | 157 | def ln_pme_nav( 158 | amounts: _AmountArray, 159 | index: _AmountArray, 160 | ) -> float: 161 | ... 162 | 163 | 164 | def ln_pme_nav_2( 165 | contributions: _AmountArray, 166 | distributions: _AmountArray, 167 | index: _AmountArray, 168 | ) -> float: 169 | ... 170 | 171 | 172 | def ln_pme( 173 | amounts: _AmountArray, 174 | index: _AmountArray, 175 | ) -> Optional[float]: 176 | ... 177 | 178 | 179 | def ln_pme_2( 180 | contributions: _AmountArray, 181 | distributions: _AmountArray, 182 | index: _AmountArray, 183 | ) -> Optional[float]: 184 | ... 185 | 186 | 187 | def direct_alpha( 188 | amounts: _AmountArray, 189 | index: _AmountArray, 190 | nav: _Amount = 0, 191 | ) -> Optional[float]: 192 | ... 193 | 194 | 195 | def direct_alpha_2( 196 | contributions: _AmountArray, 197 | distributions: _AmountArray, 198 | index: _AmountArray, 199 | nav: _Amount = 0, 200 | ) -> Optional[float]: 201 | ... 202 | -------------------------------------------------------------------------------- /python/pyxirr/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anexen/pyxirr/f012d1499f449024ad92b42a26f571d58ba330b2/python/pyxirr/py.typed -------------------------------------------------------------------------------- /src/broadcasting.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use ndarray::{ArrayD, ArrayViewD, Axis, CowArray, IxDyn}; 4 | use numpy::{npyffi, Element, PyArrayDyn, PY_ARRAY_API}; 5 | use pyo3::{ 6 | exceptions::{PyTypeError, PyValueError}, 7 | prelude::*, 8 | types::{PyIterator, PyList, PySequence, PyTuple}, 9 | }; 10 | 11 | use crate::conversions::float_or_none; 12 | 13 | /// An error returned when the payments do not contain both negative and positive payments. 14 | #[derive(Debug)] 15 | pub struct BroadcastingError(String); 16 | 17 | impl BroadcastingError { 18 | pub fn new(shapes: &[&[usize]]) -> Self { 19 | Self(format!("{:?}", shapes)) 20 | } 21 | } 22 | 23 | impl fmt::Display for BroadcastingError { 24 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 25 | self.0.fmt(f) 26 | } 27 | } 28 | 29 | impl Error for BroadcastingError {} 30 | 31 | pub fn broadcast_shapes(shapes: &[&[usize]]) -> Option> { 32 | /* Discover the broadcast number of dimensions */ 33 | let ndim = shapes.iter().map(|s| s.len()).max()?; 34 | let mut result = vec![0; ndim]; 35 | 36 | /* Discover the broadcast shape in each dimension */ 37 | for (i, cur) in result.iter_mut().enumerate() { 38 | *cur = 1; 39 | for s in shapes.iter() { 40 | /* This prepends 1 to shapes not already equal to ndim */ 41 | if i + s.len() >= ndim { 42 | let k = i + s.len() - ndim; 43 | let tmp = s[k]; 44 | if tmp == 1 { 45 | continue; 46 | } 47 | if cur == &1 { 48 | *cur = tmp; 49 | } else if cur != &tmp { 50 | return None; 51 | } 52 | } 53 | } 54 | } 55 | 56 | Some(result) 57 | } 58 | 59 | #[macro_export] 60 | macro_rules! broadcast_together { 61 | ($($a:expr),*) => { 62 | { 63 | let _a = &[$($a.shape(),)*]; 64 | 65 | match $crate::broadcasting::broadcast_shapes(_a) { 66 | Some(shape) => Ok(( $($a.broadcast(shape.clone()).unwrap(),)*)), 67 | None => Err(BroadcastingError::new(_a)) 68 | } 69 | } 70 | }; 71 | } 72 | 73 | pub fn pyiter_to_arrayd<'p, T>(pyiter: &'p PyIterator) -> PyResult> 74 | where 75 | T: FromPyObject<'p>, 76 | { 77 | let mut dims = Vec::new(); 78 | let mut flat_list = Vec::new(); 79 | flatten_pyiter(pyiter, &mut dims, &mut flat_list, 0)?; 80 | let arr = ArrayD::from_shape_vec(IxDyn(&dims), flat_list); 81 | arr.map_err(|e| PyValueError::new_err(e.to_string())) 82 | } 83 | 84 | pub fn arrayd_to_pylist<'a>(py: Python<'a>, array: ArrayViewD<'_, f64>) -> PyResult<&'a PyList> { 85 | let list = PyList::empty(py); 86 | if array.ndim() == 1 { 87 | for &x in array { 88 | list.append(float_or_none(x).to_object(py))?; 89 | } 90 | } else { 91 | for subarray in array.axis_iter(Axis(0)) { 92 | let sublist = arrayd_to_pylist(py, subarray)?; 93 | list.append(sublist)?; 94 | } 95 | } 96 | Ok(list) 97 | } 98 | 99 | fn flatten_pyiter<'p, T>( 100 | pyiter: &'p PyIterator, 101 | shape: &mut Vec, 102 | flat_list: &mut Vec, 103 | depth: usize, 104 | ) -> PyResult<()> 105 | where 106 | T: FromPyObject<'p>, 107 | { 108 | let mut max_i = 0; 109 | for (i, item) in pyiter.enumerate() { 110 | let item = item?; 111 | max_i = i; 112 | match item.extract::() { 113 | Ok(val) => flat_list.push(val), 114 | Err(_) => { 115 | let sublist = item.iter()?; 116 | flatten_pyiter(sublist, shape, flat_list, depth + 1)?; 117 | } 118 | } 119 | } 120 | 121 | max_i += 1; 122 | if let Some(current) = shape.get(depth) { 123 | shape[depth] = (*current).max(max_i); 124 | } else { 125 | shape.push(max_i); 126 | } 127 | 128 | Ok(()) 129 | } 130 | 131 | pub enum Arg<'p, T> { 132 | Scalar(T), 133 | Array(CowArray<'p, T, IxDyn>), 134 | NumpyArray(&'p PyArrayDyn), 135 | } 136 | 137 | impl<'p, T> Arg<'p, T> 138 | where 139 | T: Clone + numpy::Element, 140 | { 141 | pub fn into_arrayd(self) -> CowArray<'p, T, IxDyn> { 142 | self.into() 143 | } 144 | } 145 | 146 | fn is_numpy_available() -> bool { 147 | Python::with_gil(|py| py.import("numpy").is_ok()) 148 | } 149 | 150 | fn pyarray_cast(ob: &PyAny) -> PyResult<&PyArrayDyn> { 151 | let ptr = unsafe { 152 | PY_ARRAY_API.PyArray_CastToType( 153 | ob.py(), 154 | ob.as_ptr() as _, 155 | U::get_dtype(ob.py()).into_dtype_ptr(), 156 | 0, 157 | ) 158 | }; 159 | if !ptr.is_null() { 160 | Ok(unsafe { PyArrayDyn::::from_owned_ptr(ob.py(), ptr) }) 161 | } else { 162 | Err(PyErr::fetch(ob.py())) 163 | } 164 | } 165 | 166 | impl<'p> FromPyObject<'p> for Arg<'p, f64> { 167 | fn extract(ob: &'p PyAny) -> PyResult { 168 | if let Ok(value) = ob.extract::() { 169 | return Ok(Arg::Scalar(value)); 170 | }; 171 | 172 | if ob.downcast::().is_ok() 173 | || ob.downcast::().is_ok() 174 | || ob.downcast::().is_ok() 175 | || ob.downcast::().is_ok() 176 | { 177 | let arr = pyiter_to_arrayd(ob.iter()?)?; 178 | return Ok(Arg::Array(CowArray::from(arr))); 179 | } 180 | 181 | if is_numpy_available() { 182 | if let Ok(a) = ob.downcast::>() { 183 | return Ok(Arg::NumpyArray(a)); 184 | } 185 | 186 | if unsafe { npyffi::PyArray_Check(ob.py(), ob.as_ptr()) } == 1 { 187 | let a = pyarray_cast::(ob)?; 188 | return Ok(Arg::NumpyArray(a)); 189 | } 190 | } 191 | 192 | Err(PyTypeError::new_err("must be float scalar or array-like")) 193 | } 194 | } 195 | 196 | impl<'p> FromPyObject<'p> for Arg<'p, bool> { 197 | fn extract(ob: &'p PyAny) -> PyResult { 198 | if let Ok(value) = ob.extract::() { 199 | return Ok(Arg::Scalar(value)); 200 | }; 201 | 202 | if ob.downcast::().is_ok() 203 | || ob.downcast::().is_ok() 204 | || ob.downcast::().is_ok() 205 | || ob.downcast::().is_ok() 206 | { 207 | let arr = pyiter_to_arrayd(ob.iter()?)?; 208 | return Ok(Arg::Array(CowArray::from(arr))); 209 | } 210 | 211 | if is_numpy_available() { 212 | if let Ok(a) = ob.downcast::>() { 213 | return Ok(Arg::NumpyArray(a)); 214 | } 215 | } 216 | 217 | Err(PyTypeError::new_err("must be bool scalar or array-like")) 218 | } 219 | } 220 | 221 | impl IntoPy for Arg<'_, f64> { 222 | fn into_py(self, py: Python<'_>) -> PyObject { 223 | self.to_object(py) 224 | } 225 | } 226 | 227 | impl ToPyObject for Arg<'_, f64> { 228 | fn to_object(&self, py: Python<'_>) -> PyObject { 229 | match self { 230 | Arg::Scalar(s) => float_or_none(*s).into_py(py), 231 | Arg::Array(a) => match arrayd_to_pylist(py, a.view()) { 232 | Ok(py_list) => py_list.into_py(py), 233 | Err(err) => err.into_py(py), 234 | }, 235 | Arg::NumpyArray(a) => a.into_py(py), 236 | } 237 | } 238 | } 239 | 240 | impl<'p, T> From> for CowArray<'p, T, IxDyn> 241 | where 242 | T: Clone + numpy::Element, 243 | { 244 | fn from(arg: Arg<'p, T>) -> Self { 245 | match arg { 246 | Arg::Scalar(value) => CowArray::from(ndarray::arr1(&[value]).into_dyn()), 247 | Arg::Array(a) => a, 248 | Arg::NumpyArray(a) => CowArray::from(unsafe { a.as_array() }), 249 | } 250 | } 251 | } 252 | 253 | impl From> for Arg<'_, T> { 254 | fn from(arr: ArrayD) -> Self { 255 | Arg::Array(CowArray::from(arr)) 256 | } 257 | } 258 | 259 | impl<'p, T> From<&'p PyArrayDyn> for Arg<'p, T> { 260 | fn from(arr: &'p PyArrayDyn) -> Self { 261 | Arg::NumpyArray(arr) 262 | } 263 | } 264 | 265 | #[cfg(test)] 266 | mod tests { 267 | use rstest::rstest; 268 | 269 | use super::*; 270 | 271 | #[rstest] 272 | fn test_broadcast_shapes() { 273 | assert_eq!(broadcast_shapes(&[&[3_usize, 2], &[2, 1]]), None); 274 | assert_eq!(broadcast_shapes(&[&[1_usize, 2], &[3, 1], &[3, 2]]), Some(vec![3_usize, 2])); 275 | assert_eq!( 276 | broadcast_shapes(&[&[6_usize, 7], &[5, 6, 1], &[7], &[5, 1, 7]]), 277 | Some(vec![5, 6, 7]) 278 | ); 279 | } 280 | 281 | #[rstest] 282 | fn test_flatten_pyiter() { 283 | Python::with_gil(|py| { 284 | let ob = py.eval("(range(i, i + 3) for i in range(3))", None, None).unwrap(); 285 | let array = pyiter_to_arrayd::(ob.iter().unwrap()).unwrap(); 286 | let expected = ndarray::array![[0, 1, 2], [1, 2, 3], [2, 3, 4]].into_dyn(); 287 | assert!(array == expected) 288 | }); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/conversions.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use numpy::PyArray1; 4 | use pyo3::{ 5 | exceptions::{PyTypeError, PyValueError}, 6 | prelude::*, 7 | types::*, 8 | }; 9 | use time::Date; 10 | 11 | use crate::core::{DateLike, DayCount}; 12 | 13 | // time::Date::from_ordinal_date(1970, 1).unwrap().to_julian_day(); 14 | static UNIX_EPOCH_JULIAN_DAY: i32 = 2440588; 15 | 16 | pub fn float_or_none(result: f64) -> Option { 17 | if result.is_nan() { 18 | None 19 | } else { 20 | Some(result) 21 | } 22 | } 23 | 24 | pub fn fallible_float_or_none(result: Result, silent: bool) -> PyResult> 25 | where 26 | pyo3::PyErr: From, 27 | { 28 | match result { 29 | Err(e) => { 30 | if silent { 31 | Ok(None) 32 | } else { 33 | Err(e.into()) 34 | } 35 | } 36 | Ok(v) => Ok(float_or_none(v)), 37 | } 38 | } 39 | 40 | #[derive(FromPyObject)] 41 | pub enum PyDayCount { 42 | String(String), 43 | DayCount(DayCount), 44 | } 45 | 46 | impl TryInto for PyDayCount { 47 | type Error = PyErr; 48 | 49 | fn try_into(self) -> Result { 50 | match self { 51 | PyDayCount::String(s) => DayCount::of(&s), 52 | PyDayCount::DayCount(d) => Ok(d), 53 | } 54 | } 55 | } 56 | 57 | #[pymethods] 58 | impl DayCount { 59 | #[staticmethod] 60 | fn of(value: &str) -> PyResult { 61 | DayCount::from_str(value).map_err(PyValueError::new_err) 62 | } 63 | 64 | fn __str__(&self) -> String { 65 | self.to_string() 66 | } 67 | } 68 | 69 | struct DaysSinceUnixEpoch(i32); 70 | 71 | impl<'s> FromPyObject<'s> for DaysSinceUnixEpoch { 72 | fn extract(obj: &'s PyAny) -> PyResult { 73 | obj.extract::().map(|x| Self(x as i32)) 74 | } 75 | } 76 | 77 | impl From for DateLike { 78 | fn from(value: DaysSinceUnixEpoch) -> Self { 79 | Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + value.0).unwrap().into() 80 | } 81 | } 82 | 83 | impl From for DateLike { 84 | fn from(value: i64) -> Self { 85 | Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + (value as i32)).unwrap().into() 86 | } 87 | } 88 | 89 | impl From<&PyDate> for DateLike { 90 | fn from(value: &PyDate) -> Self { 91 | let date = Date::from_calendar_date( 92 | value.get_year(), 93 | value.get_month().try_into().unwrap(), 94 | value.get_day(), 95 | ) 96 | .unwrap(); 97 | date.into() 98 | } 99 | } 100 | 101 | impl<'s> FromPyObject<'s> for DateLike { 102 | fn extract(obj: &'s PyAny) -> PyResult { 103 | if let Ok(py_date) = obj.downcast::() { 104 | return Ok(py_date.into()); 105 | } 106 | 107 | if let Ok(py_string) = obj.downcast::() { 108 | return py_string 109 | .to_str()? 110 | .parse::() 111 | .map_err(|e| PyValueError::new_err(e.to_string())); 112 | } 113 | 114 | match obj.get_type().name()? { 115 | "datetime64" => Ok(obj 116 | .call_method1("astype", ("datetime64[D]",))? 117 | .call_method1("astype", ("int32",))? 118 | .extract::()? 119 | .into()), 120 | 121 | "Timestamp" => Ok(obj.call_method0("to_pydatetime")?.downcast::()?.into()), 122 | 123 | other => Err(PyTypeError::new_err(format!( 124 | "Type {:?} is not understood. Expected: date", 125 | other 126 | ))), 127 | } 128 | } 129 | } 130 | 131 | fn extract_iterable<'a, T>(values: &'a PyAny) -> PyResult> 132 | where 133 | T: FromPyObject<'a>, 134 | { 135 | values.iter()?.map(|i| i.and_then(PyAny::extract::)).collect() 136 | } 137 | 138 | fn extract_date_series_from_numpy(series: &PyAny) -> PyResult> { 139 | Ok(series 140 | .call_method1("astype", ("datetime64[D]",))? 141 | .call_method1("astype", ("int32",))? 142 | .downcast::>()? 143 | .readonly() 144 | .as_slice()? 145 | .iter() 146 | .map(|&x| DateLike::from(DaysSinceUnixEpoch(x))) 147 | .collect()) 148 | } 149 | 150 | pub fn extract_date_series(series: &PyAny) -> PyResult> { 151 | match series.get_type().name()? { 152 | "Series" => extract_date_series_from_numpy(series.getattr("values")?), 153 | "ndarray" => extract_date_series_from_numpy(series), 154 | _ => extract_iterable::(series), 155 | } 156 | } 157 | 158 | fn extract_amount_series_from_numpy(series: &PyAny) -> PyResult> { 159 | Ok(series 160 | .call_method1("astype", ("float64",))? 161 | .extract::<&PyArray1>()? 162 | .readonly() 163 | .to_vec()?) 164 | } 165 | 166 | fn extract_records(data: &PyAny) -> PyResult<(Vec, Vec)> { 167 | let capacity = if let Ok(capacity) = data.len() { 168 | capacity 169 | } else { 170 | 0 171 | }; 172 | 173 | let mut _dates: Vec = if capacity > 0 { 174 | Vec::with_capacity(capacity) 175 | } else { 176 | Vec::new() 177 | }; 178 | let mut _amounts: Vec = if capacity > 0 { 179 | Vec::with_capacity(capacity) 180 | } else { 181 | Vec::new() 182 | }; 183 | 184 | for obj in data.iter()? { 185 | let obj = obj?; 186 | // get_item() uses different ffi calls for different objects 187 | // PyTuple.get_item (ffi::PyTuple_GetItem) is faster than PyAny.get_item (ffi::PyObject_GetItem) 188 | let tup = if let Ok(py_tuple) = obj.downcast::() { 189 | (py_tuple.get_item(0)?, py_tuple.get_item(1)?) 190 | } else if let Ok(py_list) = obj.downcast::() { 191 | (py_list.get_item(0)?, py_list.get_item(1)?) 192 | } else { 193 | (obj.get_item(0)?, obj.get_item(1)?) 194 | }; 195 | 196 | _dates.push(tup.0.extract::()?); 197 | _amounts.push(tup.1.extract::()?); 198 | } 199 | 200 | Ok((_dates, _amounts)) 201 | } 202 | 203 | pub struct AmountArray(Vec); 204 | 205 | impl AmountArray { 206 | pub fn into_vec(self) -> Vec { 207 | self.0 208 | } 209 | } 210 | 211 | impl<'s> FromPyObject<'s> for AmountArray { 212 | fn extract(obj: &'s PyAny) -> PyResult { 213 | extract_amount_series(obj).map(AmountArray) 214 | } 215 | } 216 | 217 | impl std::ops::Deref for AmountArray { 218 | type Target = [f64]; 219 | 220 | fn deref(&self) -> &[f64] { 221 | self.0.as_ref() 222 | } 223 | } 224 | 225 | pub fn extract_amount_series(series: &PyAny) -> PyResult> { 226 | match series.get_type().name()? { 227 | "Series" => extract_amount_series_from_numpy(series.getattr("values")?), 228 | "ndarray" => extract_amount_series_from_numpy(series), 229 | _ => extract_iterable::(series), 230 | } 231 | } 232 | 233 | pub fn extract_payments( 234 | dates: &PyAny, 235 | amounts: Option<&PyAny>, 236 | ) -> PyResult<(Vec, Vec)> { 237 | if amounts.is_some() { 238 | return Ok((extract_date_series(dates)?, extract_amount_series(amounts.unwrap())?)); 239 | }; 240 | 241 | if let Ok(py_dict) = dates.downcast::() { 242 | return Ok(( 243 | extract_iterable::(py_dict.keys())?, 244 | extract_iterable::(py_dict.values())?, 245 | )); 246 | } 247 | 248 | match dates.get_type().name()? { 249 | "DataFrame" => { 250 | let frame = dates; 251 | let columns = frame.getattr("columns")?; 252 | Ok(( 253 | extract_date_series(frame.get_item(columns.get_item(0)?)?)?, 254 | extract_amount_series(frame.get_item(columns.get_item(1)?)?)?, 255 | )) 256 | } 257 | "Series" 258 | if dates 259 | .getattr("index") 260 | .and_then(|index| index.get_type().name()) 261 | .unwrap_or("unknown") 262 | == "DatetimeIndex" => 263 | { 264 | Ok((extract_date_series(dates.getattr("index")?)?, extract_amount_series(dates)?)) 265 | } 266 | "ndarray" => { 267 | let array = dates; 268 | Ok(( 269 | extract_date_series(array.get_item(0)?)?, 270 | extract_amount_series(array.get_item(1)?)?, 271 | )) 272 | } 273 | _ => extract_records(dates), 274 | } 275 | } 276 | 277 | #[cfg(test)] 278 | mod tests { 279 | use pyo3::{prelude::*, types::PyDict}; 280 | use rstest::rstest; 281 | use time::{Date, Month}; 282 | 283 | use crate::core::DateLike; 284 | 285 | fn get_locals<'p>(py: &'p Python) -> &'p PyDict { 286 | py.eval("{ 'np': __import__('numpy') }", None, None).unwrap().downcast::().unwrap() 287 | } 288 | 289 | #[rstest] 290 | #[cfg_attr(feature = "nonumpy", ignore)] 291 | fn test_extract_from_numpy_datetime_array() { 292 | Python::with_gil(|py| { 293 | let locals = get_locals(&py); 294 | let data = py 295 | .eval( 296 | "np.array(['2007-02-01', '2009-09-30'], dtype='datetime64[D]')", 297 | Some(locals), 298 | None, 299 | ) 300 | .unwrap(); 301 | let dt: Vec = data.extract().unwrap(); 302 | let exp: DateLike = Date::from_calendar_date(2007, Month::February, 1).unwrap().into(); 303 | 304 | assert_eq!(dt[0], exp); 305 | }) 306 | } 307 | 308 | #[rstest] 309 | #[cfg_attr(feature = "nonumpy", ignore)] 310 | fn test_extract_from_numpy_datetime() { 311 | Python::with_gil(|py| { 312 | let locals = get_locals(&py); 313 | let data = py.eval("np.datetime64('2007-02-01', '[D]')", Some(locals), None).unwrap(); 314 | let dt: DateLike = data.extract().unwrap(); 315 | let exp: DateLike = Date::from_calendar_date(2007, Month::February, 1).unwrap().into(); 316 | 317 | assert_eq!(dt, exp); 318 | }) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | // TODO: move core module into a separate crate 2 | 3 | mod models; 4 | mod optimize; 5 | pub mod periodic; 6 | mod scheduled; 7 | mod utils; 8 | 9 | pub use models::{DateLike, InvalidPaymentsError}; 10 | pub use periodic::*; 11 | pub use scheduled::*; 12 | pub mod private_equity; 13 | -------------------------------------------------------------------------------- /src/core/models.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt, str::FromStr}; 2 | 3 | use time::{macros::format_description, Date}; 4 | 5 | #[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Copy)] 6 | pub struct DateLike(Date); 7 | 8 | impl From for Date { 9 | fn from(val: DateLike) -> Self { 10 | val.0 11 | } 12 | } 13 | 14 | impl From for DateLike { 15 | fn from(value: Date) -> Self { 16 | Self(value) 17 | } 18 | } 19 | 20 | impl AsRef for DateLike { 21 | fn as_ref(&self) -> &Date { 22 | &self.0 23 | } 24 | } 25 | 26 | impl FromStr for DateLike { 27 | type Err = time::error::Parse; 28 | 29 | fn from_str(s: &str) -> Result { 30 | // get only date part: yyyy-mm-dd 31 | // this allows to parse datetime strings 32 | let s = if s.len() > 10 { 33 | &s[0..10] 34 | } else { 35 | s 36 | }; 37 | 38 | // try %Y-%m-%d 39 | if let Ok(d) = Date::parse(s, &format_description!("[year]-[month]-[day]")) { 40 | return Ok(d.into()); 41 | } 42 | 43 | // try %m/%d/%Y 44 | Ok(Date::parse(s, &format_description!("[month]/[day]/[year]"))?.into()) 45 | } 46 | } 47 | 48 | /// An error returned when the payments do not contain both negative and positive payments. 49 | #[derive(Clone, Debug)] 50 | pub struct InvalidPaymentsError(String); 51 | 52 | impl InvalidPaymentsError { 53 | pub fn new(message: T) -> Self { 54 | Self(message.to_string()) 55 | } 56 | } 57 | 58 | impl fmt::Display for InvalidPaymentsError { 59 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 60 | self.0.fmt(f) 61 | } 62 | } 63 | 64 | impl Error for InvalidPaymentsError {} 65 | 66 | pub fn validate(payments: &[f64], dates: Option<&[DateLike]>) -> Result<(), InvalidPaymentsError> { 67 | if let Some(dates) = dates { 68 | validate_length(payments, dates)?; 69 | } 70 | validate_positive_negative(payments) 71 | } 72 | 73 | pub fn validate_length(payments: &[f64], dates: &[DateLike]) -> Result<(), InvalidPaymentsError> { 74 | if payments.len() != dates.len() { 75 | Err(InvalidPaymentsError::new("the amounts and dates arrays are of different lengths")) 76 | } else { 77 | Ok(()) 78 | } 79 | } 80 | 81 | pub fn validate_positive_negative(payments: &[f64]) -> Result<(), InvalidPaymentsError> { 82 | let positive = payments.iter().any(|&p| p > 0.0); 83 | let negative = payments.iter().any(|&p| p < 0.0); 84 | 85 | if positive && negative { 86 | Ok(()) 87 | } else { 88 | Err(InvalidPaymentsError::new("negative and positive payments are required")) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/core/optimize.rs: -------------------------------------------------------------------------------- 1 | const MAX_ERROR: f64 = 1e-9; 2 | const MAX_ITERATIONS: u32 = 50; 3 | const MAX_FX_TOL: f64 = 1e-3; 4 | 5 | pub fn newton_raphson(start: f64, f: &Func, d: &Deriv) -> f64 6 | where 7 | Func: Fn(f64) -> f64, 8 | Deriv: Fn(f64) -> f64, 9 | { 10 | // x[n + 1] = x[n] - f(x[n])/f'(x[n]) 11 | 12 | let mut x = start; 13 | 14 | for _ in 0..MAX_ITERATIONS { 15 | let y = f(x); 16 | 17 | if y.abs() < MAX_ERROR { 18 | return x; 19 | } 20 | 21 | let delta = y / d(x); 22 | 23 | if delta.abs() < MAX_ERROR { 24 | return x - delta; 25 | } 26 | 27 | x -= delta; 28 | } 29 | 30 | f64::NAN 31 | } 32 | 33 | // a slightly modified version that accepts a callback function that 34 | // calculates the result and the derivative at once 35 | pub fn newton_raphson_2(start: f64, fd: &Func) -> f64 36 | where 37 | Func: Fn(f64) -> (f64, f64), 38 | { 39 | // x[n + 1] = x[n] - f(x[n])/f'(x[n]) 40 | 41 | let mut x = start; 42 | 43 | for _ in 0..MAX_ITERATIONS { 44 | let (y0, y1) = fd(x); 45 | 46 | if y0.abs() < MAX_ERROR { 47 | return x; 48 | } 49 | 50 | let delta = y0 / y1; 51 | 52 | if delta.abs() < MAX_ERROR && y0.abs() < MAX_FX_TOL { 53 | return x; 54 | } 55 | 56 | x -= delta; 57 | } 58 | 59 | f64::NAN 60 | } 61 | 62 | pub fn newton_raphson_with_default_deriv(start: f64, f: Func) -> f64 63 | where 64 | Func: Fn(f64) -> f64, 65 | { 66 | // deriv = (f(x + e) - f(x - e))/((x + e) - x) 67 | // multiply denominator by 2 for faster convergence 68 | 69 | // https://programmingpraxis.com/2012/01/13/excels-xirr-function/ 70 | 71 | let df = |x| (f(x + MAX_ERROR) - f(x - MAX_ERROR)) / (2.0 * MAX_ERROR); 72 | newton_raphson(start, &f, &df) 73 | } 74 | 75 | // https://github.com/scipy/scipy/blob/39bf11b96f771dcecf332977fb2c7843a9fd55f2/scipy/optimize/Zeros/brentq.c 76 | pub fn brentq(f: &Func, xa: f64, xb: f64, iter: usize) -> f64 77 | where 78 | Func: Fn(f64) -> f64, 79 | { 80 | const XTOL: f64 = 2e-14; 81 | const RTOL: f64 = 8.881784197001252e-16; 82 | 83 | let mut xpre = xa; 84 | let mut xcur = xb; 85 | let (mut xblk, mut fblk, mut spre, mut scur) = (0., 0., 0., 0.); 86 | /* the tolerance is 2*delta */ 87 | 88 | let mut fpre = f(xpre); 89 | let mut fcur = f(xcur); 90 | 91 | if fpre.signum() == fcur.signum() { 92 | return f64::NAN; // sign error 93 | } 94 | if fpre == 0. { 95 | return xpre; 96 | } 97 | if fcur == 0. { 98 | return xcur; 99 | } 100 | 101 | for _ in 0..iter { 102 | if fpre != 0. && fcur != 0. && fpre.signum() != fcur.signum() { 103 | xblk = xpre; 104 | fblk = fpre; 105 | spre = xcur - xpre; 106 | scur = spre; 107 | } 108 | 109 | if fblk.abs() < fcur.abs() { 110 | xpre = xcur; 111 | xcur = xblk; 112 | xblk = xpre; 113 | 114 | fpre = fcur; 115 | fcur = fblk; 116 | fblk = fpre; 117 | } 118 | 119 | let delta = (XTOL + RTOL * xcur.abs()) / 2.; 120 | let sbis = (xblk - xcur) / 2.; 121 | 122 | if fcur == 0. || sbis.abs() < delta { 123 | return if fcur.abs() < MAX_FX_TOL { 124 | xcur 125 | } else { 126 | f64::NAN 127 | }; 128 | } 129 | 130 | if spre.abs() > delta && fcur.abs() < fpre.abs() { 131 | let stry = if xpre == xblk { 132 | /* interpolate */ 133 | -fcur * (xcur - xpre) / (fcur - fpre) 134 | } else { 135 | /* extrapolate */ 136 | let dpre = (fpre - fcur) / (xpre - xcur); 137 | let dblk = (fblk - fcur) / (xblk - xcur); 138 | -fcur * (fblk * dblk - fpre * dpre) / (dblk * dpre * (fblk - fpre)) 139 | }; 140 | 141 | if 2. * stry.abs() < spre.abs().min(3. * sbis.abs() - delta) { 142 | /* good short step */ 143 | spre = scur; 144 | scur = stry; 145 | } else { 146 | /* bisect */ 147 | spre = sbis; 148 | scur = sbis; 149 | } 150 | } else { 151 | /* bisect */ 152 | spre = sbis; 153 | scur = sbis; 154 | } 155 | 156 | xpre = xcur; 157 | fpre = fcur; 158 | if scur.abs() > delta { 159 | xcur += scur; 160 | } else { 161 | xcur += if sbis > 0. { 162 | delta 163 | } else { 164 | -delta 165 | } 166 | } 167 | 168 | fcur = f(xcur); 169 | } 170 | 171 | f64::NAN 172 | } 173 | 174 | pub fn brentq_grid_search<'a, Func>( 175 | breakpoints: &'a [&[f64]], 176 | f: &'a Func, 177 | ) -> impl Iterator + 'a 178 | where 179 | Func: Fn(f64) -> f64 + 'a, 180 | { 181 | breakpoints 182 | .iter() 183 | .flat_map(|x| x.windows(2).map(|pair| brentq(f, pair[0], pair[1], 100))) 184 | .filter(|r| r.is_finite() && f(*r).abs() < 1e-3) 185 | } 186 | 187 | // use std::f64::consts::PI; 188 | // 189 | // use num_complex::Complex; 190 | 191 | // pub fn durand_kerner(coefficients: &[f64]) -> Vec { 192 | // // https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/numerical_methods/durand_kerner_roots.cpp#L109 193 | // 194 | // // numerical errors less when the first coefficient is "1" 195 | // // hence, we normalize the first coefficient 196 | // let coefficients: Vec<_> = coefficients.iter().map(|x| x / coefficients[0]).collect(); 197 | // let degree = coefficients.len() - 1; 198 | // let accuracy = 1e-10; 199 | // 200 | // let mut roots: Vec<_> = (0..degree) 201 | // .into_iter() 202 | // .map(|i| Complex::::new(PI * (i as f64 / degree as f64), 0.0)) 203 | // .collect(); 204 | // 205 | // let mut prev_delta = f64::INFINITY; 206 | // 207 | // for _ in 0..MAX_ITERATIONS { 208 | // let mut tol_condition = 0.0f64; 209 | // 210 | // for n in 0..degree { 211 | // let numerator = polyval(&coefficients, roots[n]); 212 | // 213 | // let mut denominator = Complex::new(1.0, 0.0); 214 | // for i in 0..degree { 215 | // if i != n { 216 | // denominator *= roots[n] - roots[i]; 217 | // } 218 | // } 219 | // 220 | // let delta = numerator / denominator; 221 | // 222 | // if !delta.norm().is_finite() { 223 | // break; 224 | // } 225 | // 226 | // roots[n] -= delta; 227 | // 228 | // tol_condition = tol_condition.max(delta.norm()) 229 | // } 230 | // 231 | // if (prev_delta - tol_condition).abs() <= accuracy || tol_condition < accuracy { 232 | // break; 233 | // } 234 | // 235 | // prev_delta = tol_condition 236 | // } 237 | // 238 | // roots.into_iter().map(|x| x.norm()).collect() 239 | // } 240 | 241 | // valuate a polynomial at specific values. 242 | // fn polyval(coefficients: &[f64], x: Complex) -> Complex { 243 | // let degree = coefficients.len() - 1; 244 | // coefficients.iter().enumerate().map(|(i, c)| c * x.powf((degree - i) as f64)).sum() 245 | // } 246 | 247 | // #[cfg(test)] 248 | // mod tests { 249 | // use super::*; 250 | // use assert_approx_eq::assert_approx_eq; 251 | // use rstest::rstest; 252 | // 253 | // #[rstest] 254 | // fn test_durand_kerner() { 255 | // let cf = &[-1e6, 5000., -3.]; 256 | // let roots = durand_kerner(cf); 257 | // 258 | // dbg!(&roots); 259 | // for root in roots { 260 | // let guess = root - 1.; 261 | // dbg!(guess); 262 | // let rate = crate::core::irr(cf, Some(guess)).unwrap(); 263 | // assert_approx_eq!(crate::core::npv(rate, cf, None), 0.0); 264 | // } 265 | // } 266 | // } 267 | -------------------------------------------------------------------------------- /src/core/scheduled/mod.rs: -------------------------------------------------------------------------------- 1 | mod day_count; 2 | mod xirr; 3 | mod xnfv; 4 | 5 | pub use day_count::{days_between, year_fraction, DayCount}; 6 | pub use xirr::*; 7 | pub use xnfv::*; 8 | -------------------------------------------------------------------------------- /src/core/scheduled/xirr.rs: -------------------------------------------------------------------------------- 1 | use super::{year_fraction, DayCount}; 2 | use crate::core::{ 3 | models::{validate, validate_length, DateLike, InvalidPaymentsError}, 4 | optimize::{brentq, newton_raphson_2}, 5 | utils::{fast_pow, initial_guess}, 6 | }; 7 | 8 | pub fn xirr( 9 | dates: &[DateLike], 10 | amounts: &[f64], 11 | guess: Option, 12 | day_count: Option, 13 | ) -> Result { 14 | validate(amounts, Some(dates))?; 15 | 16 | let deltas = &day_count_factor(dates, day_count); 17 | 18 | if amounts.len() == 2 { 19 | return Ok(xirr_analytical_2(amounts, deltas)); 20 | } 21 | 22 | let f = |rate| xnpv_result(amounts, deltas, rate); 23 | let fd = |rate| xnpv_result_with_deriv(amounts, deltas, rate); 24 | 25 | let guess = guess.unwrap_or_else(|| initial_guess(amounts)); 26 | let rate = newton_raphson_2(guess, &fd); 27 | 28 | if rate.is_finite() { 29 | return Ok(rate); 30 | } 31 | 32 | let rate = brentq(&f, -0.999999999999999, 100., 100); 33 | 34 | if rate.is_finite() { 35 | return Ok(rate); 36 | } 37 | 38 | let mut step = 0.01; 39 | let mut guess = -0.99999999999999; 40 | while guess < 1.0 { 41 | let rate = newton_raphson_2(guess, &fd); 42 | if rate.is_finite() { 43 | return Ok(rate); 44 | } 45 | guess += step; 46 | step = (step * 1.1).min(0.1); 47 | } 48 | 49 | Ok(f64::NAN) 50 | } 51 | 52 | fn xirr_analytical_2(amounts: &[f64], deltas: &[f64]) -> f64 { 53 | // solve analytically: 54 | // cf[0]/(1+r)^d[0] + cf[1]/(1+r)^d[1] = 0 => 55 | // cf[1]/(1+r)^d[1] = -cf[0]/(1+r)^d[0] => rearrange 56 | // cf[1]/cf[0] = -(1+r)^d[1]/(1+r)^d[0] => simplify 57 | // cf[1]/cf[0] = -(1+r)^(d[1] - d[0]) => take the root 58 | // (cf[1]/cf[0])^(1/(d[1] - d[0])) = -(1 + r) => multiply by -1 and subtract 1 59 | // r = -(cf[1]/cf[0])^(1/(d[1] - d[0])) - 1 60 | (-amounts[1] / amounts[0]).powf(1. / (deltas[1] - deltas[0])) - 1.0 61 | } 62 | 63 | /// Calculate the net present value of a series of payments at irregular intervals. 64 | pub fn xnpv( 65 | rate: f64, 66 | dates: &[DateLike], 67 | amounts: &[f64], 68 | day_count: Option, 69 | ) -> Result { 70 | validate_length(amounts, dates)?; 71 | 72 | let deltas = &day_count_factor(dates, day_count); 73 | Ok(xnpv_result(amounts, deltas, rate)) 74 | } 75 | 76 | pub fn sign_changes(v: &[f64]) -> i32 { 77 | v.windows(2) 78 | .map(|p| (p[0].is_finite() && p[1].is_finite() && p[0].signum() != p[1].signum()) as i32) 79 | .sum() 80 | } 81 | 82 | pub fn zero_crossing_points(v: &[f64]) -> Vec { 83 | v.windows(2) 84 | .enumerate() 85 | .filter_map(|(i, p)| { 86 | (p[0].is_finite() && p[1].is_finite() && p[0].signum() != p[1].signum()).then_some(i) 87 | }) 88 | .collect() 89 | } 90 | 91 | fn day_count_factor(dates: &[DateLike], day_count: Option) -> Vec { 92 | let min_date = dates.iter().min().unwrap(); 93 | let dc = day_count.unwrap_or_default(); 94 | dates.iter().map(|d| year_fraction(&min_date, &d, dc)).collect() 95 | } 96 | 97 | // \sum_{i=1}^n \frac{P_i}{(1 + rate)^{(d_i - d_0)/365}} 98 | fn xnpv_result(payments: &[f64], deltas: &[f64], rate: f64) -> f64 { 99 | if rate <= -1.0 { 100 | // bound newton_raphson 101 | return f64::INFINITY; 102 | } 103 | payments.iter().zip(deltas).map(|(p, &e)| p * fast_pow(1.0 + rate, -e)).sum() 104 | } 105 | 106 | // XNPV first derivative 107 | // \sum_{i=1}^n P_i * (d_0 - d_i) / 365 * (1 + rate)^{((d_0 - d_i)/365 - 1)}} 108 | // simplify in order to reuse cached deltas (d_i - d_0)/365 109 | // \sum_{i=1}^n \frac{P_i * -(d_i - d_0) / 365}{(1 + rate)^{((d_i - d_0)/365 + 1)}} 110 | // fn xnpv_result_deriv(payments: &[f64], deltas: &[f64], rate: f64) -> f64 { 111 | // payments.iter().zip(deltas).map(|(p, e)| p * -e * fast_pow(1.0 + rate, -e - 1.0)).sum() 112 | // } 113 | 114 | fn xnpv_result_with_deriv(payments: &[f64], deltas: &[f64], rate: f64) -> (f64, f64) { 115 | if rate <= -1.0 { 116 | return (f64::INFINITY, f64::INFINITY); 117 | } 118 | // pow is an expensive function. 119 | // we can re-use the result of pow for derivative calculation 120 | payments.iter().zip(deltas).fold((0.0, 0.0), |acc, (p, e)| { 121 | let y0 = p * fast_pow(1.0 + rate, -e); 122 | let y1 = y0 * -e / (1.0 + rate); 123 | (acc.0 + y0, acc.1 + y1) 124 | }) 125 | } 126 | 127 | #[cfg(test)] 128 | mod tests { 129 | use super::*; 130 | use rstest::rstest; 131 | 132 | #[rstest] 133 | fn test_sign_changes() { 134 | assert_eq!(sign_changes(&[1., 2., 3.]), 0); 135 | assert_eq!(sign_changes(&[1., 2., -3.]), 1); 136 | assert_eq!(sign_changes(&[1., -2., 3.]), 2); 137 | assert_eq!(sign_changes(&[-1., 2., -3.]), 2); 138 | assert_eq!(sign_changes(&[-1., -2., -3.]), 0); 139 | assert_eq!(sign_changes(&[1., f64::NAN, 3.]), 0); 140 | } 141 | 142 | #[rstest] 143 | fn test_zero_crossing_points() { 144 | assert_eq!(zero_crossing_points(&[1., 2., 3.]), vec![]); 145 | assert_eq!(zero_crossing_points(&[1., -2., -3.]), vec![0]); 146 | assert_eq!(zero_crossing_points(&[1., -2., 3.]), vec![0, 1]); 147 | assert_eq!(zero_crossing_points(&[-1., -2., 3.]), vec![1]); 148 | assert_eq!(zero_crossing_points(&[1., f64::NAN, 3.]), vec![]); 149 | 150 | assert_eq!( 151 | zero_crossing_points(&[7., 6., -3., -4., -7., 8., 3., -6., 7., 8.]), 152 | vec![1, 4, 6, 7], 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/core/scheduled/xnfv.rs: -------------------------------------------------------------------------------- 1 | use super::{year_fraction, DayCount}; 2 | use crate::core::{ 3 | models::{validate, DateLike, InvalidPaymentsError}, 4 | periodic::fv, 5 | }; 6 | 7 | // http://westclintech.com/SQL-Server-Financial-Functions/SQL-Server-XFV-function 8 | pub fn xfv( 9 | start_date: &DateLike, 10 | cash_flow_date: &DateLike, 11 | end_date: &DateLike, 12 | cash_flow_rate: f64, 13 | end_rate: f64, 14 | cash_flow: f64, 15 | day_count: Option, 16 | ) -> f64 { 17 | let dc = day_count.unwrap_or_default(); 18 | let yf1 = year_fraction(start_date, end_date, dc); 19 | let yf2 = year_fraction(start_date, cash_flow_date, dc); 20 | let fv1 = self::fv(end_rate, yf1, 0., -1., false); 21 | let fv2 = self::fv(cash_flow_rate, yf2, 0., -1., false); 22 | fv1 / fv2 * cash_flow 23 | } 24 | 25 | // http://westclintech.com/SQL-Server-Financial-Functions/SQL-Server-XNFV-function 26 | pub fn xnfv( 27 | rate: f64, 28 | dates: &[DateLike], 29 | amounts: &[f64], 30 | day_count: Option, 31 | ) -> Result { 32 | validate(amounts, Some(dates))?; 33 | let d1 = dates.iter().min().unwrap(); 34 | let d2 = dates.iter().max().unwrap(); 35 | let periods = year_fraction(d1, d2, day_count.unwrap_or_default()); 36 | let pv = super::xnpv(rate, dates, amounts, None)?; 37 | Ok(self::fv(rate, periods, 0., -pv, false)) 38 | } 39 | -------------------------------------------------------------------------------- /src/core/utils.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | pub(crate) fn non_zero_range(p: &[f64]) -> Range { 4 | let n = p.len(); 5 | let first_non_zero_index = p.iter().position(|&x| x != 0.0).unwrap_or(n); 6 | let last_non_zero_index = n - p.iter().rev().position(|&x| x != 0.0).unwrap_or(n); 7 | first_non_zero_index..last_non_zero_index 8 | } 9 | 10 | pub(crate) fn trim_zeros(p: &[f64]) -> &[f64] { 11 | &p[non_zero_range(p)] 12 | } 13 | 14 | pub(crate) fn fast_pow(a: f64, b: f64) -> f64 { 15 | // works only if a is positive 16 | (a.log2() * b).exp2() 17 | } 18 | 19 | pub(crate) fn scale(values: &[f64], factor: f64) -> Vec { 20 | values.iter().map(|v| v * factor).collect() 21 | } 22 | 23 | pub(crate) fn sum_pairwise_mul(a: &[f64], b: &[f64]) -> f64 { 24 | a.iter().zip(b).map(|(x, y)| x * y).sum() 25 | } 26 | 27 | pub(crate) fn pairwise_mul(a: &[f64], b: &[f64]) -> Vec { 28 | a.iter().zip(b).map(|(x, y)| x * y).collect() 29 | } 30 | 31 | pub(crate) fn series_signum(a: &[f64]) -> f64 { 32 | // returns -1. if any item is negative, otherwise +1. 33 | if a.iter().any(|x| x.is_sign_negative()) { 34 | -1. 35 | } else { 36 | 1. 37 | } 38 | } 39 | 40 | pub(crate) fn sum_negatives_positives(values: &[f64]) -> (f64, f64) { 41 | values.iter().fold((0., 0.), |acc, x| { 42 | if x.is_sign_negative() { 43 | (acc.0 + x, acc.1) 44 | } else { 45 | (acc.0, acc.1 + x) 46 | } 47 | }) 48 | } 49 | 50 | pub(crate) fn initial_guess(values: &[f64]) -> f64 { 51 | let (outflows, inflows) = sum_negatives_positives(values); 52 | let guess = inflows / -outflows - 1.0; 53 | guess.clamp(-0.9, 0.1) 54 | } 55 | 56 | pub(crate) fn is_a_good_rate(rate: f64, f: F) -> bool 57 | where 58 | F: Fn(f64) -> f64, 59 | { 60 | rate.is_finite() && f(rate).abs() < 1e-3 61 | } 62 | -------------------------------------------------------------------------------- /tests/common/cases.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | pub fn irr_expected_result(input: &str) -> f64 { 4 | match input { 5 | "tests/samples/unordered.csv" => 0.7039842300, 6 | "tests/samples/random_100.csv" => 2.3320600601, 7 | "tests/samples/random_1000.csv" => 0.8607558299, 8 | "tests/samples/minus_0_993.csv" => -0.995697224362268, 9 | _ => panic!(), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/common/helpers.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use pyo3::{once_cell::GILOnceCell, prelude::*, types::*}; 3 | 4 | #[macro_export] 5 | macro_rules! py_dict { 6 | ($py:expr) => { 7 | ::pyo3::types::PyDict::new($py) 8 | }; 9 | ($py:expr, $($key:expr => $value:expr), *) => { 10 | { 11 | let _dict = ::pyo3::types::PyDict::new($py); 12 | $( 13 | _dict.set_item($key, $value).unwrap(); 14 | )* 15 | _dict 16 | } 17 | }; 18 | } 19 | 20 | #[macro_export] 21 | macro_rules! py_dict_merge { 22 | ($py:expr, $($dict:expr), *) => { 23 | { 24 | let _dict = ::pyo3::types::PyDict::new($py); 25 | $( 26 | _dict.getattr("update").unwrap().call1(($dict,)).unwrap(); 27 | )* 28 | _dict 29 | 30 | } 31 | } 32 | } 33 | 34 | #[macro_export] 35 | macro_rules! pyxirr_call { 36 | ($py:expr, $name:expr, $args:expr) => {{ 37 | let kwargs = ::pyo3::types::PyDict::new($py); 38 | pyxirr_call!($py, $name, $args, kwargs) 39 | }}; 40 | ($py:expr, $name:expr, $args:expr, $kwargs:expr) => { 41 | pyxirr_call_impl!($py, $name, $args, $kwargs).unwrap().extract().unwrap() 42 | }; 43 | } 44 | 45 | #[macro_export] 46 | macro_rules! pyxirr_call_impl { 47 | ($py:expr, $name:expr, $args:expr) => {{ 48 | let kwargs = ::pyo3::types::PyDict::new($py); 49 | pyxirr_call_impl!($py, $name, $args, kwargs) 50 | }}; 51 | ($py:expr, $name:expr, $args:expr, $kwargs:expr) => {{ 52 | use common::get_pyxirr_func; 53 | get_pyxirr_func($py, $name).call($args, Some($kwargs)) 54 | }}; 55 | } 56 | 57 | #[macro_export] 58 | macro_rules! assert_almost_eq { 59 | ($a:expr, $b:expr, $eps:expr) => {{ 60 | let (a, b, eps) = (&$a, &$b, $eps); 61 | assert!((*a - *b).abs() < eps, "assertion failed: `({} !~= {})`", *a, *b); 62 | }}; 63 | ($a:expr, $b:expr) => {{ 64 | let (a, b) = (&$a, &$b); 65 | let eps: f64 = 1e-9; 66 | assert!((*a - *b).abs() < eps, "assertion failed: `({} !~= {})`", *a, *b); 67 | }}; 68 | } 69 | 70 | #[macro_export] 71 | macro_rules! assert_future_value { 72 | ($rate:expr, $nper:expr, $pmt:expr, $pv:expr, $fv:expr, $pmt_at_beginning:expr) => {{ 73 | let (rate, nper, pmt, pv, fv, pmt_at_beginning) = 74 | ($rate, $nper, $pmt, $pv, $fv, $pmt_at_beginning); 75 | 76 | let fv = fv.unwrap_or(0.0); 77 | 78 | if rate == 0.0 { 79 | assert_almost_eq!(fv + pv + pmt * nper, 0.0); 80 | return; 81 | } 82 | 83 | let pmt_at_beginning = if pmt_at_beginning.unwrap_or(false) { 84 | 1.0 85 | } else { 86 | 0.0 87 | }; 88 | 89 | let result = fv 90 | + pv * f64::powf(1.0 + rate, nper) 91 | + pmt * (1.0 + rate * pmt_at_beginning) / rate * (f64::powf(1.0 + rate, nper) - 1.0); 92 | 93 | assert_almost_eq!(result, 0.0, 1e-6); 94 | }}; 95 | } 96 | 97 | static PYXIRR: GILOnceCell> = GILOnceCell::new(); 98 | 99 | pub fn get_pyxirr_module(py: Python) -> &PyModule { 100 | PYXIRR 101 | .get_or_init(py, || { 102 | let module = PyModule::new(py, "pyxirr").unwrap(); 103 | pyxirr::pyxirr(py, module).unwrap(); 104 | module.into() 105 | }) 106 | .as_ref(py) 107 | } 108 | 109 | pub fn get_pyxirr_func<'p>(py: Python<'p>, name: &str) -> &'p PyCFunction { 110 | get_pyxirr_module(py).getattr(name).unwrap().downcast().unwrap() 111 | } 112 | 113 | pub fn pd_read_csv<'p>(py: Python<'p>, input_file: &str) -> &'p PyAny { 114 | let locals = py_dict!(py, 115 | "sample" => PyString::new(py, input_file), 116 | "pd" => PyModule::import(py, "pandas").unwrap() 117 | ); 118 | 119 | py.eval("pd.read_csv(sample, header=None, parse_dates=[0])", Some(locals), None).unwrap() 120 | } 121 | 122 | pub struct PaymentsLoader<'p> { 123 | py: Python<'p>, 124 | data: Vec<&'p PyTuple>, 125 | } 126 | 127 | impl<'p> PaymentsLoader<'p> { 128 | pub fn from_csv(py: Python<'p>, input_file: &str) -> Self { 129 | let data = Self::from_py_csv(py, input_file).unwrap(); 130 | Self { 131 | py, 132 | data, 133 | } 134 | } 135 | 136 | fn from_py_csv(py: Python<'p>, input_file: &str) -> PyResult> { 137 | let strptime = py.import("datetime")?.getattr("datetime")?.getattr("strptime")?; 138 | let reader = py.import("csv")?.getattr("reader")?; 139 | let builtins = py.import("builtins")?; 140 | let file_obj = builtins.getattr("open")?.call1((input_file,))?; 141 | 142 | let data = reader 143 | .call1((file_obj,))? 144 | .iter()? 145 | .map(|r| { 146 | let r = r.unwrap(); 147 | let date = strptime.call1((r.get_item(0)?, "%Y-%m-%d"))?; 148 | let amount = builtins.getattr("float")?.call1((r.get_item(1)?,))?; 149 | Ok(PyTuple::new(py, vec![date, amount])) 150 | }) 151 | .collect(); 152 | 153 | file_obj.call_method0("close")?; 154 | 155 | data 156 | } 157 | 158 | pub fn to_records(&self) -> &'p PyAny { 159 | PyList::new(self.py, &self.data).as_ref() 160 | } 161 | 162 | pub fn to_dict(&self) -> &'p PyAny { 163 | PyDict::from_sequence(self.py, self.to_records().into()).unwrap().as_ref() 164 | } 165 | 166 | pub fn to_columns(&self) -> (&'p PyAny, &'p PyAny) { 167 | ( 168 | PyList::new(self.py, self.data.iter().map(|x| x.get_item(0).unwrap())).as_ref(), 169 | PyList::new(self.py, self.data.iter().map(|x| x.get_item(1).unwrap())).as_ref(), 170 | ) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | mod cases; 2 | mod helpers; 3 | 4 | #[allow(unused_imports)] 5 | pub use cases::*; 6 | pub use helpers::*; 7 | -------------------------------------------------------------------------------- /tests/samples/1938.csv: -------------------------------------------------------------------------------- 1 | 1937-07-02,-10 2 | 1937-07-03,10 3 | 1937-07-04,1338.17 4 | 1938-07-29,10147.5 5 | 1938-08-11,-578133.57 6 | 1938-08-12,-12559.95 7 | 1938-04-22,1519.64 8 | 1938-05-22,10147.5 9 | 1938-06-22,-11500 10 | 1938-10-22,544.47 11 | 1938-08-28,701.78 12 | 1938-09-09,-600.68 13 | 1938-09-10,-601.95 14 | 1938-09-11,-518.2 15 | 1938-11-10,36000 16 | 1938-11-12,22308.68 17 | 1938-12-11,-753.37 18 | 1939-01-07,-20.05 19 | 1939-02-10,-43.15 20 | 1939-03-11,-742.98 21 | 1939-08-17,-11220 22 | 1939-10-17,1.27 23 | 1939-04-04,11220 24 | 1939-04-09,-3846.05 25 | 1939-04-12,1058.75 26 | 1939-06-18,-2.94 27 | 1939-05-11,-36000 28 | 1939-05-12,-15016.3 29 | 1939-06-11,-6.49 30 | 1939-08-02,-36.72 31 | 1939-08-10,-72.54 32 | 1939-08-12,15892.29 33 | 1939-08-26,-88.71 34 | 1939-09-08,-53.5 35 | 1939-09-10,-260.09 36 | 1939-11-01,-3.85 37 | 1939-11-12,-289.11 38 | 1939-12-10,-46.99 39 | 1939-12-13,63.32 40 | 1940-01-10,63.32 41 | 1940-02-09,621.47 42 | 1940-02-10,-383.63 43 | 1940-03-11,63.32 44 | 1940-04-11,63.32 45 | 1940-05-11,419.94 46 | 1940-08-11,-4.87 47 | 1941-01-10,1507.56 48 | 1941-09-12,-14.49 49 | 1940-11-30,-1.31 50 | 1941-08-01,7064.85 51 | 1941-08-11,-0.82 52 | 1941-11-10,3554.11 53 | 1942-02-10,16891.26 54 | -------------------------------------------------------------------------------- /tests/samples/30-0.csv: -------------------------------------------------------------------------------- 1 | 2020-12-28,333.27030402162 2 | 2022-01-19,-146.496109518728 3 | 2028-05-23,-189.180345798348 4 | 2028-11-20,-3916.85381200519 5 | 2026-10-13,547.9687115377 6 | 2026-05-03,256.967998484194 7 | 2038-06-18,-1007.94859579087 8 | 2028-08-19,2849.92238832545 9 | 2033-11-04,-2926.11813748505 10 | 2028-07-07,-2100.03939636663 11 | 2030-04-08,-1774.95425947525 12 | 2026-06-12,1153.30842235563 13 | 2031-05-16,-850.79736879218 14 | 2022-07-27,969.005094263762 15 | 2026-04-14,-2434.27493500624 16 | 2037-01-25,-2662.70254731969 17 | 2033-01-12,-1571.67477329839 18 | 2024-07-21,212.342250278806 19 | 2026-10-22,150.24119768385 20 | 2033-12-06,52.861370652219 21 | 2031-10-03,529.176148783966 22 | 2023-10-20,-792.671066147255 23 | 2033-12-09,-253.722818574121 24 | 2030-01-26,-600.59020869209 25 | 2029-03-14,1803.61216958492 26 | 2032-10-30,-1410.63591380165 27 | 2038-06-26,-405.164179378513 28 | 2031-05-07,-921.827086493052 29 | 2032-01-25,4.035153939964 30 | 2023-11-22,3374.22648669238 31 | -------------------------------------------------------------------------------- /tests/samples/30-1.csv: -------------------------------------------------------------------------------- 1 | 2022-01-11,1666.6503340053 2 | 2026-12-25,2585.93561849736 3 | 2025-06-08,-782.413642487316 4 | 2027-04-29,-2625.00527834325 5 | 2027-08-02,6.817194014249 6 | 2027-12-30,1264.35501137407 7 | 2026-02-21,649.756006562289 8 | 2029-12-30,672.588635499639 9 | 2036-09-13,-2887.02617166351 10 | 2023-12-21,959.41424430624 11 | 2027-04-18,-31.596822319005 12 | 2028-10-22,566.602861584151 13 | 2026-07-29,-1493.06835434469 14 | 2023-10-03,1028.88472253072 15 | 2029-08-10,-1921.90083415199 16 | 2035-10-25,1156.22545680743 17 | 2024-08-05,-474.322848831017 18 | 2038-03-13,-523.871018796651 19 | 2032-09-19,-284.678550742256 20 | 2041-08-21,-4071.81770364425 21 | 2036-04-01,-603.053507173341 22 | 2036-10-20,-1793.72409443228 23 | 2039-10-26,-556.050877300056 24 | 2038-01-17,1316.95241397017 25 | 2035-09-16,3406.843958165 26 | 2038-06-10,-1775.8763056634 27 | 2022-11-24,-105.966164702126 28 | 2032-05-29,-48.461847355557 29 | 2026-08-02,-3906.7843710352 30 | 2040-05-01,-2592.45040409947 31 | -------------------------------------------------------------------------------- /tests/samples/30-10.csv: -------------------------------------------------------------------------------- 1 | 2028-12-05,618.745882085621 2 | 2030-08-15,-1369.79586234343 3 | 2029-11-18,164.688757414672 4 | 2036-05-20,-2988.09343176693 5 | 2044-08-24,1038.68249209244 6 | 2043-09-09,189.855069245047 7 | 2035-07-12,-412.158624400451 8 | 2033-09-15,-2532.78001422464 9 | 2043-12-27,-860.363497965613 10 | 2046-04-10,461.122484632866 11 | 2040-04-15,-982.619556731916 12 | 2039-02-05,-190.873516981859 13 | 2043-10-20,93.788651211132 14 | 2031-04-09,-967.695740861598 15 | 2040-06-02,-56.415809670707 16 | 2030-01-25,1061.08139397161 17 | 2031-08-21,28.888183191574 18 | 2041-08-28,-1989.07763209179 19 | 2029-07-25,1850.49136859386 20 | 2035-01-07,3580.29302495974 21 | 2038-09-07,3309.46400323226 22 | 2042-08-04,239.609105051574 23 | 2046-12-01,-1756.94031790732 24 | 2038-11-20,366.703675109898 25 | 2045-12-06,-425.564964709568 26 | 2043-01-25,-4212.08241338709 27 | 2029-03-15,278.265804652426 28 | 2044-11-15,1520.41770744509 29 | 2046-09-20,-322.637027674109 30 | 2029-10-17,-216.28241187533 31 | -------------------------------------------------------------------------------- /tests/samples/30-11.csv: -------------------------------------------------------------------------------- 1 | 2028-03-08,918.190658246446 2 | 2032-07-07,-2494.04555129367 3 | 2043-08-01,4576.81976084901 4 | 2045-10-07,1797.91900733261 5 | 2040-07-23,-1120.45030593738 6 | 2030-03-20,-2731.23965261413 7 | 2047-12-28,933.346710293429 8 | 2031-02-12,-3302.07756054455 9 | 2046-06-21,22.122782008648 10 | 2039-01-21,-44.539068185843 11 | 2034-03-10,-3238.02848164166 12 | 2032-11-22,816.342047428701 13 | 2029-11-11,-1070.35274245477 14 | 2035-08-17,-1012.7431093917 15 | 2046-04-20,-1057.00877068691 16 | 2046-02-03,283.225573332953 17 | 2039-05-14,342.853658984884 18 | 2035-01-01,950.83605591287 19 | 2045-07-16,343.373039223001 20 | 2033-06-10,67.603582318002 21 | 2043-11-16,-1511.75445775792 22 | 2041-02-12,-986.537431412707 23 | 2037-11-16,-231.986151462213 24 | 2044-05-21,1608.42510704057 25 | 2039-07-16,316.912887014371 26 | 2042-03-08,-961.097770449565 27 | 2035-02-17,-1839.15078182113 28 | 2044-11-17,-3147.03304829172 29 | 2038-09-29,-246.103574868949 30 | 2036-06-05,1686.37521571121 31 | -------------------------------------------------------------------------------- /tests/samples/30-12.csv: -------------------------------------------------------------------------------- 1 | 2029-06-08,-1626.00123207172 2 | 2034-03-14,2055.19288922676 3 | 2029-09-30,-826.776627291127 4 | 2037-04-17,1105.15010652678 5 | 2031-01-11,833.495920846374 6 | 2038-06-13,351.985894627136 7 | 2045-06-26,-959.283837082209 8 | 2039-02-27,1865.39287923091 9 | 2045-02-24,705.218333287466 10 | 2039-03-23,520.24988108564 11 | 2047-07-08,-542.123428796374 12 | 2041-07-08,446.813144826431 13 | 2045-12-15,-494.269592255484 14 | 2035-09-03,1488.00811059792 15 | 2033-05-30,-2331.92945397769 16 | 2048-02-03,-524.912207443741 17 | 2033-06-06,-1198.02099043244 18 | 2041-05-16,-1055.17406771968 19 | 2035-09-03,-3230.04739660018 20 | 2034-02-20,-1014.98965334046 21 | 2041-10-11,1096.45654418065 22 | 2048-06-15,1727.84410942768 23 | 2037-07-06,-964.917670821845 24 | 2042-06-09,-1513.61519642752 25 | 2042-06-27,-2030.30580972669 26 | 2038-10-19,-582.51932848522 27 | 2048-04-17,431.939524834698 28 | 2047-10-27,4262.07952153729 29 | 2046-06-12,410.082462691107 30 | 2036-03-02,-391.38962270489 31 | -------------------------------------------------------------------------------- /tests/samples/30-13.csv: -------------------------------------------------------------------------------- 1 | 2020-10-08,-320.409336468929 2 | 2034-02-11,-1168.0309461329 3 | 2035-02-23,-116.435806487608 4 | 2032-05-22,1082.82589946232 5 | 2025-09-23,1021.31187983928 6 | 2033-08-11,330.693013409593 7 | 2037-01-01,357.608023904653 8 | 2022-06-10,4019.47530700281 9 | 2035-08-11,-636.194921783809 10 | 2030-03-06,1797.12740657937 11 | 2033-02-12,-1363.62400631159 12 | 2020-10-09,1643.85326162288 13 | 2039-02-12,380.183155427822 14 | 2028-07-23,86.989799456888 15 | 2030-12-23,-1655.5145291891 16 | 2028-11-25,282.738535669003 17 | 2025-09-19,1503.78463596763 18 | 2036-01-10,-1641.59028688468 19 | 2038-10-31,-318.274660250744 20 | 2030-02-20,257.569362953608 21 | 2023-06-30,-1413.67502554896 22 | 2033-01-25,255.335845901369 23 | 2021-10-26,-1576.27975587903 24 | 2021-03-24,318.335920888297 25 | 2033-06-01,-3433.96218604721 26 | 2027-03-09,-114.644608755237 27 | 2038-04-26,-224.006933326095 28 | 2031-05-06,275.321216777982 29 | 2021-03-16,1327.43942336165 30 | 2024-10-19,1401.88316736762 31 | -------------------------------------------------------------------------------- /tests/samples/30-14.csv: -------------------------------------------------------------------------------- 1 | 2022-07-17,521.605278379593 2 | 2025-11-12,334.291978242097 3 | 2029-10-11,-2672.10264985228 4 | 2030-02-04,-2025.99102498481 5 | 2037-06-01,-170.906609493473 6 | 2025-04-16,-428.074751907286 7 | 2022-09-21,-227.568538110101 8 | 2028-01-07,-862.278953104916 9 | 2032-01-20,416.405185156392 10 | 2036-11-07,-41.639060644952 11 | 2038-03-19,643.457136202465 12 | 2036-04-26,-875.236497153228 13 | 2031-08-26,98.876068558997 14 | 2039-09-23,-1167.36723009687 15 | 2040-01-14,604.696247394783 16 | 2041-05-04,-4221.94929769699 17 | 2023-12-27,-829.808176790775 18 | 2035-04-28,330.539560232388 19 | 2031-03-18,-226.080676806153 20 | 2025-01-05,1457.68443274085 21 | 2031-03-20,-333.532522242255 22 | 2029-03-27,-816.111224875307 23 | 2041-12-10,362.764002073326 24 | 2040-04-24,321.236289346612 25 | 2026-07-24,-979.131863392987 26 | 2034-01-09,-1914.4455314066 27 | 2031-11-25,362.272362429902 28 | 2038-04-13,-2853.52475877018 29 | 2032-02-22,218.759181686587 30 | 2029-11-06,215.675839312328 31 | -------------------------------------------------------------------------------- /tests/samples/30-15.csv: -------------------------------------------------------------------------------- 1 | 2024-09-30,1273.6305313285 2 | 2034-03-29,-906.638012924116 3 | 2034-04-20,-217.011045451774 4 | 2039-10-04,-1574.14257532463 5 | 2030-09-05,-1167.1162055966 6 | 2033-10-29,82.469324854138 7 | 2043-10-24,3494.17880217973 8 | 2026-01-27,-1965.09628510356 9 | 2030-06-26,2162.29703372969 10 | 2038-06-16,-1980.64287682228 11 | 2034-09-21,0.479756750409 12 | 2034-07-16,468.265115796124 13 | 2027-08-15,-547.262083575523 14 | 2042-09-30,112.044967090662 15 | 2043-08-27,1687.31088550415 16 | 2033-11-24,633.549634970679 17 | 2036-06-14,-1958.59039123904 18 | 2041-08-26,-514.865487092504 19 | 2041-10-19,-3281.43596090889 20 | 2028-06-22,2172.2592036951 21 | 2042-06-08,-1748.63417106777 22 | 2030-06-28,-540.701779925455 23 | 2036-09-05,-389.641219962412 24 | 2038-04-18,-793.305106155516 25 | 2029-02-20,194.442552085288 26 | 2029-02-11,-191.629158967675 27 | 2040-12-16,258.212309730837 28 | 2040-02-25,1749.30509341587 29 | 2027-09-21,2213.90724311398 30 | 2029-12-10,320.160604504894 31 | -------------------------------------------------------------------------------- /tests/samples/30-16.csv: -------------------------------------------------------------------------------- 1 | 2026-08-14,-3125.27322119937 2 | 2041-04-02,2773.02043726138 3 | 2041-07-26,-1790.36692905916 4 | 2030-03-02,696.520587025705 5 | 2041-04-14,320.162076329341 6 | 2034-04-23,2254.26241732869 7 | 2041-04-13,4073.84064341955 8 | 2037-05-30,679.382339172679 9 | 2039-01-21,-93.516950687653 10 | 2026-12-01,2463.19559710923 11 | 2042-09-23,795.956504328337 12 | 2034-06-06,926.897724010813 13 | 2034-03-26,-564.600506196181 14 | 2039-10-25,1823.42969522893 15 | 2030-01-04,-1918.55814421891 16 | 2030-07-09,786.907879863191 17 | 2040-03-30,-70.904739136152 18 | 2042-07-05,-257.181723217839 19 | 2043-04-03,-3777.59109844083 20 | 2044-04-22,-99.354920690123 21 | 2037-12-09,3486.68795131705 22 | 2045-09-17,-1065.43548332919 23 | 2036-12-31,2444.91826028308 24 | 2040-06-10,-202.82385265153 25 | 2039-11-09,43.093325061539 26 | 2029-11-22,1162.52815201015 27 | 2037-07-02,-3155.94494628772 28 | 2032-06-22,4238.73552154618 29 | 2045-09-05,-600.459261046623 30 | 2043-03-16,-136.640636165578 31 | -------------------------------------------------------------------------------- /tests/samples/30-17.csv: -------------------------------------------------------------------------------- 1 | 2025-11-13,-672.795451352152 2 | 2033-09-10,-90.985854348913 3 | 2032-04-26,1130.33286654848 4 | 2032-10-19,742.721437796862 5 | 2037-06-22,564.407440781116 6 | 2038-07-02,-1212.18625805335 7 | 2032-11-16,-586.024183386595 8 | 2039-06-30,-1779.27859074903 9 | 2043-10-12,-3065.35827525507 10 | 2043-04-14,32.444173361525 11 | 2027-07-06,140.270615554314 12 | 2039-05-31,251.302554422917 13 | 2039-03-15,425.587054315722 14 | 2042-07-13,65.423621312111 15 | 2034-05-10,857.856025144158 16 | 2038-09-29,1560.62877038396 17 | 2034-03-08,-87.920396325637 18 | 2030-07-06,2248.31596740074 19 | 2044-04-27,-20.011922984064 20 | 2033-08-02,935.221343444048 21 | 2027-01-01,123.040408798934 22 | 2026-06-10,265.658327408675 23 | 2031-03-15,-471.502286796125 24 | 2029-10-25,804.436845713844 25 | 2038-09-16,-17.656167306972 26 | 2033-05-17,-2.533878721036 27 | 2029-09-30,187.070225606786 28 | 2027-06-12,3625.40390718999 29 | 2027-12-02,846.81990580456 30 | 2041-05-23,-1844.7735021358 31 | -------------------------------------------------------------------------------- /tests/samples/30-18.csv: -------------------------------------------------------------------------------- 1 | 2023-06-17,923.988201177555 2 | 2024-12-27,9.786739527567 3 | 2037-10-10,-786.633376584306 4 | 2028-04-11,-1356.24462485913 5 | 2036-07-10,166.893860910203 6 | 2025-10-12,3686.90057561238 7 | 2024-11-13,390.256113482262 8 | 2033-12-28,-2423.64274517353 9 | 2042-04-13,1588.71211794346 10 | 2040-12-24,1644.59503189252 11 | 2037-06-08,-2524.87857777369 12 | 2027-01-12,-2265.51509088023 13 | 2024-04-05,-995.153207836828 14 | 2026-07-05,-3374.84209105582 15 | 2040-06-05,-642.81913915829 16 | 2030-02-21,4653.91092052786 17 | 2028-10-29,3.440704980552 18 | 2030-04-19,-1073.10013321875 19 | 2037-07-15,-269.112686561538 20 | 2032-11-10,373.232593142572 21 | 2038-10-03,-1164.9890357721 22 | 2036-11-16,1018.06723407105 23 | 2037-12-21,903.855123186139 24 | 2039-09-18,-209.485444638539 25 | 2033-04-14,-2609.48406124231 26 | 2033-06-25,-1354.42897981437 27 | 2040-03-29,-2473.71999283526 28 | 2042-07-26,93.874323748294 29 | 2024-06-01,376.066178504797 30 | 2032-09-25,1448.99130485295 31 | -------------------------------------------------------------------------------- /tests/samples/30-19.csv: -------------------------------------------------------------------------------- 1 | 2029-02-03,360.770738588745 2 | 2047-08-06,4326.13861339415 3 | 2044-06-10,109.224110449316 4 | 2029-11-17,8.573684486705 5 | 2036-11-08,-31.226864158833 6 | 2034-12-08,515.024988482596 7 | 2038-02-16,-1400.46749238251 8 | 2034-08-03,-130.120338233015 9 | 2041-08-28,-465.864830826328 10 | 2041-05-23,17.289588615482 11 | 2039-10-03,943.308841962959 12 | 2041-04-12,2003.65083688856 13 | 2043-08-24,-150.761467195326 14 | 2035-12-06,-1057.98223900847 15 | 2042-10-23,-530.807419136727 16 | 2031-08-19,-1350.58272170808 17 | 2046-01-17,393.101024451778 18 | 2042-07-15,1424.27442903037 19 | 2036-01-11,317.05406096835 20 | 2040-02-20,-1830.12284413297 21 | 2046-05-03,-3036.14357500671 22 | 2037-03-09,-3912.1873429835 23 | 2041-12-21,2800.70424803283 24 | 2033-03-19,-1657.50630128841 25 | 2030-08-06,311.11669906019 26 | 2042-06-08,-170.566947601506 27 | 2038-02-17,508.662550455232 28 | 2038-10-28,1912.1409318455 29 | 2042-12-02,-1328.5839936919 30 | 2047-07-19,1010.19501524443 31 | -------------------------------------------------------------------------------- /tests/samples/30-2.csv: -------------------------------------------------------------------------------- 1 | 2021-03-28,-2638.33940539817 2 | 2031-04-03,-881.479868043996 3 | 2029-07-19,1297.6918807873 4 | 2031-02-27,2894.08843528864 5 | 2025-04-10,-2627.65875238505 6 | 2034-01-14,-2145.76580249994 7 | 2034-11-26,3258.03285153309 8 | 2038-05-19,339.523543441513 9 | 2030-12-09,299.217164069727 10 | 2029-04-06,-1973.77832386713 11 | 2030-10-24,-497.703327314113 12 | 2031-07-07,3889.00189893453 13 | 2032-11-08,756.778046433379 14 | 2025-02-03,-946.165796336019 15 | 2037-09-24,-876.093859850552 16 | 2027-03-21,633.552197169145 17 | 2038-04-02,-159.085226410324 18 | 2024-02-17,-1048.10307004258 19 | 2024-07-10,1205.69558000118 20 | 2033-11-12,651.078466801533 21 | 2022-11-21,266.006794232827 22 | 2023-07-09,-3396.65304376508 23 | 2023-09-09,-2168.81265333374 24 | 2034-01-08,591.744380111002 25 | 2036-05-11,-143.323079064037 26 | 2021-04-13,-1067.71043659263 27 | 2032-05-27,-2.401102119264 28 | 2040-06-18,2644.0098777816 29 | 2029-11-24,1532.08975471698 30 | 2038-03-07,-30.015456706744 31 | -------------------------------------------------------------------------------- /tests/samples/30-20.csv: -------------------------------------------------------------------------------- 1 | 2024-09-09,-203.365347602624 2 | 2030-12-24,1208.31894521971 3 | 2027-01-06,62.911729089493 4 | 2037-03-04,23.214035363974 5 | 2035-11-13,-1797.01688426533 6 | 2041-06-22,-2017.27457461588 7 | 2037-12-19,3997.91924991427 8 | 2028-04-10,1115.24111568803 9 | 2040-09-15,-1915.56819372739 10 | 2042-02-02,1484.15862672955 11 | 2035-02-27,1247.32460407966 12 | 2035-02-04,-42.316649900985 13 | 2037-05-15,973.000278031422 14 | 2036-08-01,1169.69300353766 15 | 2024-12-03,-24.233785734572 16 | 2041-12-20,-1130.58496750037 17 | 2031-11-01,-265.351566140742 18 | 2036-11-28,37.001024894264 19 | 2026-01-10,555.940111271138 20 | 2030-08-18,-1794.45399928709 21 | 2032-07-16,-1953.03088397946 22 | 2042-06-06,-171.922891863423 23 | 2029-10-09,-1910.9048070744 24 | 2033-03-20,343.762478283701 25 | 2026-07-30,2453.50647798804 26 | 2026-07-13,-2607.11784778847 27 | 2038-02-26,2697.77682033517 28 | 2042-03-22,-2529.313275386 29 | 2033-04-05,606.724921312875 30 | 2043-05-27,188.397614961121 31 | -------------------------------------------------------------------------------- /tests/samples/30-21.csv: -------------------------------------------------------------------------------- 1 | 2022-11-06,1053.95331231848 2 | 2039-11-26,2470.05084751896 3 | 2041-12-27,-1165.46789185096 4 | 2030-06-04,33.522854152371 5 | 2030-09-11,827.961270920807 6 | 2023-10-17,-4686.55074671876 7 | 2041-10-29,271.663519443629 8 | 2038-07-28,366.882441602998 9 | 2030-12-07,413.143597573192 10 | 2030-10-31,1063.06496211092 11 | 2024-10-05,-1403.53681078577 12 | 2041-05-31,2367.05602110489 13 | 2039-10-25,-114.427375738258 14 | 2034-11-06,235.232838586539 15 | 2024-11-06,-1040.50540503273 16 | 2023-01-07,-114.12499358534 17 | 2040-11-12,302.20678676303 18 | 2027-01-06,-17.096171520624 19 | 2040-09-14,10.51752104222 20 | 2030-12-24,-333.221449687875 21 | 2032-02-09,1884.36482626779 22 | 2031-11-16,2504.53636673252 23 | 2024-09-17,-23.171679608354 24 | 2037-03-16,1698.08545182244 25 | 2028-02-06,2273.55327736724 26 | 2039-10-25,1821.4960082549 27 | 2028-05-10,-992.076207931287 28 | 2028-02-20,-42.040846372164 29 | 2023-07-12,-1217.56465875853 30 | 2029-02-26,-1618.47738259093 31 | -------------------------------------------------------------------------------- /tests/samples/30-22.csv: -------------------------------------------------------------------------------- 1 | 2023-05-29,-1060.54818333979 2 | 2036-05-03,2959.26712504621 3 | 2040-11-22,1222.57618506468 4 | 2039-11-10,-1123.53425617784 5 | 2042-12-04,-1780.97829452609 6 | 2032-06-09,-3212.62875390706 7 | 2042-08-17,1062.90497372753 8 | 2035-09-03,109.102478922819 9 | 2028-02-02,-20.535038765884 10 | 2031-10-02,523.670290931378 11 | 2037-01-17,1051.04044993527 12 | 2033-05-04,-3301.70192621163 13 | 2039-03-13,2686.59062261943 14 | 2025-04-15,-1371.27807590072 15 | 2040-09-14,-3284.80444243792 16 | 2036-06-26,1906.03107988512 17 | 2024-07-07,328.612266673726 18 | 2032-12-09,-168.870311093353 19 | 2033-09-15,-149.308893053607 20 | 2040-04-17,1000.37419962204 21 | 2039-06-28,410.696251895955 22 | 2029-12-28,506.075743244442 23 | 2041-04-25,2190.13207152111 24 | 2042-12-25,-398.199495063005 25 | 2031-09-30,-548.336078273255 26 | 2032-05-19,-692.999884262619 27 | 2038-07-18,3358.92237685657 28 | 2031-07-03,-984.732660037331 29 | 2025-12-04,-2049.69857843115 30 | 2042-01-11,-1218.54793112691 31 | -------------------------------------------------------------------------------- /tests/samples/30-23.csv: -------------------------------------------------------------------------------- 1 | 2020-12-10,-1662.53131340335 2 | 2026-10-01,-449.941042164638 3 | 2022-11-07,1.56849110501 4 | 2029-04-29,-1420.03053032256 5 | 2033-01-25,3491.89872530065 6 | 2033-01-10,-210.033964231723 7 | 2035-03-31,-1300.90591407897 8 | 2031-10-10,-601.357559954336 9 | 2026-07-21,-1606.2567402243 10 | 2025-05-06,-731.346036864165 11 | 2021-12-19,927.192646654993 12 | 2028-08-17,1462.45954550562 13 | 2032-10-15,725.61719438692 14 | 2033-09-09,-1123.96102133527 15 | 2032-10-04,-500.998512042821 16 | 2022-02-07,3041.57201706782 17 | 2039-07-13,3518.67234652886 18 | 2027-11-22,-495.538352355394 19 | 2028-07-28,3762.99052837635 20 | 2033-07-15,-835.619058544914 21 | 2039-07-17,-238.384474524174 22 | 2028-08-21,904.290435249489 23 | 2029-08-12,-2144.27177617836 24 | 2024-08-28,1397.85255787309 25 | 2038-05-26,-1780.22460079825 26 | 2026-07-06,-2191.19666542756 27 | 2030-01-21,-105.889404955625 28 | 2025-05-05,-1060.14322449868 29 | 2027-05-17,-11.307925099676 30 | 2038-07-03,72.042997213457 31 | -------------------------------------------------------------------------------- /tests/samples/30-24.csv: -------------------------------------------------------------------------------- 1 | 2028-08-27,-458.264061635949 2 | 2037-06-25,1112.6268729895 3 | 2043-06-08,95.581733784404 4 | 2042-12-19,1155.67253067093 5 | 2042-02-02,2730.10200603587 6 | 2029-03-29,3879.34842604975 7 | 2046-04-11,214.700243968084 8 | 2039-10-01,2220.40370830784 9 | 2046-08-21,1617.41732789653 10 | 2046-10-19,-439.586961193935 11 | 2038-12-24,160.195742843983 12 | 2031-11-08,206.529021227208 13 | 2038-11-22,-323.424840796338 14 | 2040-10-24,126.577690868027 15 | 2031-03-09,2479.30150965747 16 | 2040-08-08,-191.92876390633 17 | 2031-09-28,737.110729453406 18 | 2042-07-30,721.931743391569 19 | 2036-11-15,-562.323515901169 20 | 2047-04-02,2473.70174737179 21 | 2045-11-08,30.546777997745 22 | 2047-06-19,2676.85211044656 23 | 2047-12-18,-1256.0040408865 24 | 2037-06-12,-0.035112489804 25 | 2039-09-06,-2235.10271921833 26 | 2046-12-02,-846.502979825036 27 | 2029-09-11,-1570.87820074649 28 | 2031-11-07,-963.887701205139 29 | 2045-11-28,1362.72340033156 30 | 2030-05-31,3195.0385527154 31 | -------------------------------------------------------------------------------- /tests/samples/30-25.csv: -------------------------------------------------------------------------------- 1 | 2025-03-09,-87.350390604205 2 | 2044-04-08,-1464.22472361546 3 | 2037-09-15,763.557778029784 4 | 2042-04-18,-626.625739767692 5 | 2040-11-01,-1019.51033056945 6 | 2037-02-23,-2957.89123835277 7 | 2026-09-08,929.696459651462 8 | 2028-07-26,609.612616222211 9 | 2025-09-09,-1128.22907383821 10 | 2025-07-23,3227.81369399929 11 | 2040-07-21,-603.603126817137 12 | 2034-05-29,-2365.72473115958 13 | 2029-08-04,1837.72565856608 14 | 2030-02-14,2414.47872820002 15 | 2032-02-03,3085.19702042096 16 | 2042-05-30,-106.481798053827 17 | 2039-09-03,-2279.32654509943 18 | 2033-01-15,1698.45859184014 19 | 2037-03-17,-735.461645233221 20 | 2027-05-03,-34.957436715569 21 | 2042-05-01,17.005759762955 22 | 2038-01-20,-1388.82172870254 23 | 2034-04-27,-715.7347386583 24 | 2044-02-19,-1473.69614921827 25 | 2036-09-15,-1789.76567368706 26 | 2041-09-12,-774.589126018637 27 | 2042-12-25,35.466721254558 28 | 2029-08-03,1991.06320321749 29 | 2036-06-13,-458.303754093279 30 | 2032-03-06,3592.97235135765 31 | -------------------------------------------------------------------------------- /tests/samples/30-26.csv: -------------------------------------------------------------------------------- 1 | 2024-02-11,-1011.19609910038 2 | 2036-02-08,2245.15276283031 3 | 2043-08-08,-2258.53010145741 4 | 2041-06-26,3170.60777717544 5 | 2032-09-20,-4538.36617649391 6 | 2038-07-14,3357.84453675439 7 | 2024-08-01,3641.4679547108 8 | 2032-09-03,123.28470388663 9 | 2035-01-20,100.99550981683 10 | 2029-11-04,-1639.13966092132 11 | 2030-01-12,600.627176363308 12 | 2027-07-25,-31.679771739781 13 | 2025-09-05,-282.929049913281 14 | 2038-10-20,4630.67290600663 15 | 2041-07-25,18.354627380003 16 | 2028-10-29,736.77688845308 17 | 2040-04-22,3698.13497284311 18 | 2032-06-02,469.614102264379 19 | 2041-07-09,-1229.07558286461 20 | 2042-10-01,-68.084791653096 21 | 2039-06-29,-561.793379067323 22 | 2034-04-15,1269.21196945053 23 | 2024-04-09,1832.27097213632 24 | 2029-12-20,-338.76888475869 25 | 2024-11-18,-1696.90164171983 26 | 2029-03-21,-2115.91804036899 27 | 2041-02-03,2075.48373652251 28 | 2025-04-02,-1802.44850043331 29 | 2043-01-22,-1647.32439735511 30 | 2035-09-05,0.708977897542 31 | -------------------------------------------------------------------------------- /tests/samples/30-27.csv: -------------------------------------------------------------------------------- 1 | 2022-09-18,610.425365978341 2 | 2034-12-05,-58.358288020149 3 | 2040-03-23,2438.41343182107 4 | 2025-12-29,-2521.1536369717 5 | 2023-08-28,-1427.035661163 6 | 2036-04-08,-143.385988212925 7 | 2027-11-06,459.281533296077 8 | 2033-01-24,-1995.4558154453 9 | 2027-07-05,120.87852481591 10 | 2035-12-13,-3626.85887617347 11 | 2028-03-27,1604.13523869361 12 | 2039-04-30,-751.901298856746 13 | 2024-04-01,2547.28907017957 14 | 2024-06-03,-463.29407367043 15 | 2023-09-17,3735.21811704808 16 | 2035-03-30,-744.951034537494 17 | 2036-06-24,-1429.22370315644 18 | 2035-11-08,-1772.39062503376 19 | 2023-03-15,-2576.03388560606 20 | 2028-05-26,-720.221678916401 21 | 2029-01-18,989.025544847169 22 | 2037-04-08,-1510.35552356328 23 | 2033-03-27,257.666451870677 24 | 2035-10-10,1669.30293003051 25 | 2024-10-14,948.514233005057 26 | 2028-11-17,-1382.55587275215 27 | 2038-09-03,378.554836739025 28 | 2040-04-01,-1300.98729695783 29 | 2026-02-03,4169.57874608292 30 | 2025-08-11,2481.38749958429 31 | -------------------------------------------------------------------------------- /tests/samples/30-28.csv: -------------------------------------------------------------------------------- 1 | 2025-09-08,-0.943503436299 2 | 2039-03-04,-4005.39190494337 3 | 2036-04-21,374.361152823838 4 | 2041-11-29,505.584128969318 5 | 2033-08-01,-983.306696970207 6 | 2026-12-01,617.314664482651 7 | 2035-05-19,2164.52035104806 8 | 2027-08-27,-3456.42917112415 9 | 2043-04-16,3914.04884222812 10 | 2027-12-28,-581.440108240025 11 | 2030-12-25,387.335111813997 12 | 2028-04-07,-1716.17324524635 13 | 2038-06-04,-125.834554614621 14 | 2026-06-02,1812.83784732893 15 | 2028-01-04,822.339410905665 16 | 2029-04-28,-2525.30339597722 17 | 2040-11-15,-388.709310032226 18 | 2027-05-20,-104.766391662072 19 | 2040-10-08,40.586046468536 20 | 2035-05-16,-3410.39117941614 21 | 2029-06-23,-758.223669241554 22 | 2033-04-18,128.860253525305 23 | 2041-09-15,1279.75107946094 24 | 2027-07-21,100.6035701568 25 | 2041-01-11,-1116.70449459165 26 | 2040-09-10,2605.61327558148 27 | 2028-01-01,-528.95530661223 28 | 2039-08-04,-96.796739553259 29 | 2041-12-12,-4650.29377515385 30 | 2042-04-01,2607.25633665165 31 | -------------------------------------------------------------------------------- /tests/samples/30-29.csv: -------------------------------------------------------------------------------- 1 | 2025-01-21,-418.866392824121 2 | 2031-11-09,329.715083093455 3 | 2033-01-08,361.393824080264 4 | 2039-08-12,-1894.86043355482 5 | 2025-02-10,-1636.20419711823 6 | 2027-03-26,1656.85906470937 7 | 2033-04-16,1937.6095736433 8 | 2032-03-02,-434.088324303372 9 | 2029-11-29,-154.326114236021 10 | 2025-12-15,-409.070468476819 11 | 2036-11-12,220.339257156714 12 | 2025-11-17,-1517.97715301748 13 | 2039-12-13,-1073.56209902851 14 | 2033-10-18,399.034769065218 15 | 2044-10-23,-404.954118983521 16 | 2026-06-23,146.610898996747 17 | 2031-03-31,-143.020320240239 18 | 2033-06-14,-1088.49463069114 19 | 2035-11-17,-442.526700319728 20 | 2036-02-07,793.706880844256 21 | 2030-11-01,-2719.28510972401 22 | 2037-12-29,-2092.08699878905 23 | 2029-09-08,-10.393792672007 24 | 2025-06-14,754.583956957934 25 | 2042-05-29,-806.448178403085 26 | 2036-05-10,-3274.09748397869 27 | 2041-05-06,3825.40112003985 28 | 2028-06-11,-216.570487777781 29 | 2032-09-20,1376.25869240123 30 | 2030-12-08,-1266.60016217574 31 | -------------------------------------------------------------------------------- /tests/samples/30-3.csv: -------------------------------------------------------------------------------- 1 | 2023-03-01,-226.115921003477 2 | 2038-09-08,-3505.58589810368 3 | 2042-01-01,873.134008429102 4 | 2042-06-23,448.826068127876 5 | 2027-08-28,1154.53585341565 6 | 2030-09-05,2671.21286340532 7 | 2026-05-10,-1674.91340078428 8 | 2040-10-23,985.948186347131 9 | 2037-04-11,14.552050426933 10 | 2025-12-24,-140.204799516016 11 | 2035-05-01,2390.20165953684 12 | 2034-02-22,-4407.71495997784 13 | 2033-08-30,-2731.57858911988 14 | 2025-03-15,1474.04529911941 15 | 2024-02-14,807.651931712503 16 | 2032-11-23,2560.66867340721 17 | 2039-10-26,246.960257966169 18 | 2041-03-24,-1625.09264012697 19 | 2030-06-22,-665.717442646522 20 | 2025-07-03,1380.13795854322 21 | 2038-04-24,-2568.12611480185 22 | 2024-05-15,39.020264845981 23 | 2036-05-27,-852.426714796024 24 | 2034-05-14,-1699.93606381858 25 | 2038-07-17,1846.54039794318 26 | 2042-03-05,1946.32305820681 27 | 2023-09-15,61.863724158872 28 | 2031-05-16,2740.10721168154 29 | 2025-05-21,2369.22129250613 30 | 2036-03-20,1039.40499013643 31 | -------------------------------------------------------------------------------- /tests/samples/30-30.csv: -------------------------------------------------------------------------------- 1 | 2023-09-14,467.593484202711 2 | 2036-01-28,2326.00071988884 3 | 2041-11-01,194.863215080161 4 | 2028-08-27,305.479372507282 5 | 2024-07-16,353.019477138992 6 | 2041-09-10,-1047.56876322499 7 | 2038-04-01,-1265.54154352564 8 | 2042-03-22,1837.06397279528 9 | 2036-08-06,-2289.26416581759 10 | 2027-07-18,559.976339026625 11 | 2037-06-28,-920.27269017234 12 | 2036-01-21,2543.72699724084 13 | 2038-06-01,152.507079485727 14 | 2042-06-05,-19.038145748544 15 | 2034-09-09,2946.21878292572 16 | 2026-09-05,-1660.64926253921 17 | 2032-01-10,-130.241040567538 18 | 2040-01-14,-3130.19089455647 19 | 2024-01-31,2744.00308335797 20 | 2024-03-24,-4568.35184982384 21 | 2027-09-20,-95.131058936558 22 | 2028-10-28,136.744261613679 23 | 2029-08-03,1225.5731479333 24 | 2038-08-16,-1659.50210632537 25 | 2039-08-25,2305.38802519271 26 | 2035-06-09,550.952067455915 27 | 2037-08-05,711.532069234801 28 | 2037-08-04,1948.9030886863 29 | 2026-11-09,-1857.02046763022 30 | 2033-10-05,294.113978638191 31 | -------------------------------------------------------------------------------- /tests/samples/30-31.csv: -------------------------------------------------------------------------------- 1 | 2024-09-13,1685.80527173079 2 | 2043-09-24,-2018.44443256505 3 | 2039-06-12,1667.26478900129 4 | 2034-06-13,-960.305435749328 5 | 2027-12-07,-820.784288577351 6 | 2026-04-12,799.454250571379 7 | 2034-12-18,-852.655709050273 8 | 2029-04-01,1497.82148500515 9 | 2030-05-31,235.567382715854 10 | 2039-06-19,-2140.87624244713 11 | 2025-04-29,232.146191553657 12 | 2043-09-22,2202.24209849614 13 | 2027-01-21,-0.113689043043 14 | 2027-07-31,307.856916859094 15 | 2043-12-30,117.22740911847 16 | 2038-01-07,1092.37062680494 17 | 2027-04-08,2220.17055046481 18 | 2037-12-01,-739.466105154453 19 | 2039-06-24,-1563.98648758035 20 | 2032-02-21,-837.976494515096 21 | 2030-12-24,58.242078414301 22 | 2041-07-15,3056.90102222875 23 | 2032-11-12,1265.68535112346 24 | 2029-12-11,-3165.19756711916 25 | 2028-12-26,-1622.08899181759 26 | 2040-08-31,429.642098188843 27 | 2039-07-20,1367.93852276409 28 | 2032-03-09,11.12092094713 29 | 2027-02-03,-346.525326634941 30 | 2039-09-16,1956.76621819449 31 | -------------------------------------------------------------------------------- /tests/samples/30-32.csv: -------------------------------------------------------------------------------- 1 | 2028-04-06,176.992257070583 2 | 2031-09-26,2803.88743002116 3 | 2038-06-12,836.305658368175 4 | 2037-05-01,2439.4350827738 5 | 2046-06-01,224.671858891535 6 | 2035-07-28,-279.376303863564 7 | 2041-04-19,1380.55284622995 8 | 2046-07-27,483.551007001564 9 | 2045-01-21,114.054460641877 10 | 2047-11-15,-85.834634343483 11 | 2047-01-11,378.519590949257 12 | 2040-10-24,-1090.14362910201 13 | 2039-11-16,-1186.28481136725 14 | 2044-04-04,2005.40988068904 15 | 2042-01-18,-632.825499662127 16 | 2047-06-08,1760.99875553498 17 | 2047-06-15,-1684.13810738506 18 | 2035-04-06,916.672421459043 19 | 2045-10-20,-1921.48541913995 20 | 2029-06-08,1612.31538229325 21 | 2030-09-26,321.090060063032 22 | 2045-01-30,-2114.62804930222 23 | 2033-12-12,362.117894450486 24 | 2040-02-06,-33.083875771257 25 | 2036-04-04,-129.827264233305 26 | 2040-06-01,511.317489419178 27 | 2037-04-27,374.785679084629 28 | 2031-05-02,1109.23555030845 29 | 2036-06-27,-1601.82325852893 30 | 2045-04-22,-295.518087662045 31 | -------------------------------------------------------------------------------- /tests/samples/30-33.csv: -------------------------------------------------------------------------------- 1 | 2024-02-26,-2650.59234403693 2 | 2028-02-01,-4.488064929208 3 | 2028-01-24,-2958.96752706709 4 | 2035-06-30,-2787.65780853495 5 | 2031-09-24,-1188.86785457967 6 | 2037-11-26,-787.70105680336 7 | 2041-01-06,644.10988661456 8 | 2029-09-26,-340.986819897027 9 | 2041-05-25,-603.004265157067 10 | 2035-09-14,592.122206826046 11 | 2030-02-25,-2728.13870643722 12 | 2033-03-05,76.561454094643 13 | 2028-03-05,148.70081959632 14 | 2029-07-25,1086.67198684277 15 | 2024-08-28,512.857290815162 16 | 2026-02-03,802.218399793249 17 | 2039-06-12,-919.360897654678 18 | 2042-09-02,-162.979824440833 19 | 2032-02-03,43.904843176136 20 | 2028-06-23,-1206.31388085863 21 | 2043-05-21,-437.293709091014 22 | 2029-09-14,-420.430673982749 23 | 2031-08-28,-1859.59724304818 24 | 2036-05-28,-179.047700807066 25 | 2033-05-16,4.4070604184 26 | 2025-04-26,2132.87271354989 27 | 2026-10-15,-1272.45049819484 28 | 2028-12-13,164.153210914309 29 | 2041-06-13,2129.06168657157 30 | 2040-03-28,-1125.61289761949 31 | -------------------------------------------------------------------------------- /tests/samples/30-34.csv: -------------------------------------------------------------------------------- 1 | 2026-02-06,-2259.5646324033 2 | 2031-05-06,-1852.09877464642 3 | 2038-05-22,-3970.83010523535 4 | 2040-06-02,-2914.23879637836 5 | 2036-03-28,-1616.6627182931 6 | 2034-06-28,-409.166456255514 7 | 2030-04-20,-3016.84021935611 8 | 2029-06-28,1333.26361045836 9 | 2033-11-21,-596.212994619128 10 | 2033-01-16,867.480423342493 11 | 2044-11-30,-631.623319626393 12 | 2039-03-09,-1496.54983539544 13 | 2043-08-13,1200.36523030414 14 | 2030-09-21,-44.952858236871 15 | 2032-11-14,-1143.71447317309 16 | 2041-03-02,1091.05223694387 17 | 2036-11-21,-2541.90190001985 18 | 2034-06-22,-1113.74921236785 19 | 2035-01-13,-3222.53184381094 20 | 2042-01-04,2033.28246952261 21 | 2026-04-25,476.843405474095 22 | 2029-05-31,2413.98988086984 23 | 2027-04-14,1498.76099871376 24 | 2039-07-17,-205.129045032927 25 | 2037-06-05,5.202526279826 26 | 2029-01-06,-768.288047795923 27 | 2030-04-09,-813.446518869724 28 | 2044-05-09,-289.194446234303 29 | 2037-11-14,-1259.10678018112 30 | 2028-04-07,-1325.18903976709 31 | -------------------------------------------------------------------------------- /tests/samples/30-35.csv: -------------------------------------------------------------------------------- 1 | 2022-10-13,-1585.4824072971 2 | 2024-04-21,-946.179496087246 3 | 2040-04-07,710.784851443589 4 | 2034-11-04,-577.885089615889 5 | 2030-01-19,-2275.95104663355 6 | 2024-03-20,-2369.66014541167 7 | 2024-02-11,-2001.14385298953 8 | 2031-02-20,-3677.49981270146 9 | 2038-09-03,-55.823968684303 10 | 2034-01-25,-249.246597750259 11 | 2025-09-07,2054.39007694601 12 | 2026-04-23,-1548.65859617792 13 | 2029-02-14,1446.02102451773 14 | 2041-03-02,-15.689542476982 15 | 2038-03-02,-80.478687629026 16 | 2029-12-17,-3128.69402995138 17 | 2025-06-25,-4000.26744778193 18 | 2041-10-31,-580.333019027857 19 | 2027-06-08,-144.554597513616 20 | 2037-07-21,-1994.72781292624 21 | 2041-03-17,2151.7670632444 22 | 2035-12-18,-2484.24852538805 23 | 2027-03-30,-198.974319086148 24 | 2032-12-17,-179.957801110091 25 | 2040-06-08,1057.73789855937 26 | 2024-05-18,116.428621913194 27 | 2039-07-16,-1060.11958903606 28 | 2034-10-15,-473.4321668491 29 | 2030-10-01,285.056974155554 30 | 2037-07-07,357.379378146521 31 | -------------------------------------------------------------------------------- /tests/samples/30-36.csv: -------------------------------------------------------------------------------- 1 | 2020-10-30,-1019.90239526579 2 | 2020-12-22,-2606.14410645756 3 | 2032-05-21,1521.03770306174 4 | 2031-11-21,566.580968144928 5 | 2029-05-03,593.857006018355 6 | 2038-05-26,-240.894395690595 7 | 2028-08-07,-286.893471382471 8 | 2026-08-15,-863.776036185133 9 | 2032-06-24,267.04511473199 10 | 2025-05-09,-1537.08439795957 11 | 2025-07-07,-178.598393992421 12 | 2031-01-28,-331.068430808082 13 | 2033-01-20,85.352816732207 14 | 2036-02-24,2022.55328609816 15 | 2032-08-09,-69.701477844326 16 | 2023-02-07,169.771228515452 17 | 2037-10-04,-540.705239941302 18 | 2034-03-10,144.140760475494 19 | 2039-02-25,1607.95568046074 20 | 2029-02-05,-56.372880950672 21 | 2037-02-20,1007.93923594208 22 | 2039-11-20,-101.74034578643 23 | 2039-10-04,1288.0386218521 24 | 2031-05-20,-3004.46834682653 25 | 2021-02-11,-653.837138757358 26 | 2031-01-19,-1104.06159797461 27 | 2038-05-30,-1215.56111787675 28 | 2023-02-18,-3589.99386908357 29 | 2021-07-18,-266.886690933408 30 | 2022-11-23,-3426.59358478067 31 | -------------------------------------------------------------------------------- /tests/samples/30-37.csv: -------------------------------------------------------------------------------- 1 | 2029-02-23,-833.609136523079 2 | 2039-12-03,911.763674762491 3 | 2031-02-18,-2415.2479158099 4 | 2030-02-28,-5.26024226827 5 | 2048-01-18,-396.510247384726 6 | 2036-05-16,-495.419967270352 7 | 2031-08-22,1594.13186017225 8 | 2046-05-15,-501.980847458042 9 | 2034-11-09,840.197989339582 10 | 2032-07-08,-2228.35592531844 11 | 2029-11-07,653.978807837339 12 | 2032-08-11,802.60136016655 13 | 2039-11-25,-2054.37635401127 14 | 2043-05-28,151.139920540724 15 | 2037-10-21,837.548899649756 16 | 2031-02-23,-213.167861971908 17 | 2036-10-03,-1307.73571188087 18 | 2034-06-13,503.352859381649 19 | 2038-05-04,668.219419160759 20 | 2044-09-19,-1848.90116798455 21 | 2042-05-04,584.26579128562 22 | 2039-06-05,1062.66005462833 23 | 2040-09-22,-3374.64060705128 24 | 2031-10-01,-112.056276267287 25 | 2037-04-12,-3285.46588926042 26 | 2034-03-17,1821.94104962032 27 | 2045-09-18,1278.64162524391 28 | 2040-03-21,1733.71274343446 29 | 2046-07-07,2167.07815820599 30 | 2043-03-10,-460.943535037123 31 | -------------------------------------------------------------------------------- /tests/samples/30-38.csv: -------------------------------------------------------------------------------- 1 | 2027-06-21,-1258.23423874668 2 | 2031-11-27,-2407.77749232728 3 | 2028-10-24,-253.345144829253 4 | 2030-06-08,406.925813713986 5 | 2044-05-30,-542.073727727385 6 | 2033-12-09,389.185096251464 7 | 2042-09-07,-988.015015812739 8 | 2034-04-08,1665.31960193949 9 | 2031-03-05,1208.50002833908 10 | 2040-06-14,-175.813907944335 11 | 2027-11-01,-481.243401183391 12 | 2034-02-08,-2146.53412051554 13 | 2032-05-02,-1969.7148445007 14 | 2041-08-12,-3265.08456979949 15 | 2028-08-14,768.307386740727 16 | 2035-11-30,1929.29071316865 17 | 2036-05-11,539.645258023139 18 | 2043-11-03,-585.851375412566 19 | 2030-08-22,-947.810510209651 20 | 2029-01-17,1825.88062030927 21 | 2043-07-22,1924.46286656159 22 | 2031-11-07,-2194.60987308818 23 | 2035-12-19,60.127293508021 24 | 2036-07-07,2850.81921699565 25 | 2044-02-22,-808.713746882315 26 | 2027-10-21,100.755400734705 27 | 2044-10-22,-52.075130729734 28 | 2033-05-28,-105.065996367318 29 | 2038-02-12,748.407823849006 30 | 2031-04-18,-238.909962391775 31 | -------------------------------------------------------------------------------- /tests/samples/30-39.csv: -------------------------------------------------------------------------------- 1 | 2022-06-23,3464.09239754445 2 | 2025-10-24,1444.06610841254 3 | 2041-09-06,81.133833039761 4 | 2037-08-26,87.80114543286 5 | 2034-03-29,-2604.70920590268 6 | 2031-03-01,2.00302973753 7 | 2028-12-10,-2601.27634555599 8 | 2029-04-03,1629.27744965838 9 | 2024-08-23,-1412.7892851158 10 | 2030-07-09,130.371301154524 11 | 2026-11-08,-189.775028176242 12 | 2027-11-26,-266.501135389522 13 | 2033-12-11,-1029.05822621265 14 | 2025-10-20,1009.68358535468 15 | 2038-09-21,236.637616881152 16 | 2038-04-10,2879.60412586098 17 | 2035-10-19,1570.16506085605 18 | 2041-02-28,-3299.29704759448 19 | 2025-11-29,-2190.38905095398 20 | 2029-11-09,1898.68795169795 21 | 2029-02-10,35.29175846716 22 | 2028-07-31,-289.029062930691 23 | 2032-03-19,1118.09594210687 24 | 2038-07-26,-319.235335450576 25 | 2025-01-05,793.937084056142 26 | 2037-09-10,1403.84488594576 27 | 2031-11-17,4444.05637768121 28 | 2032-03-02,81.088179446232 29 | 2035-02-19,157.297556778916 30 | 2032-02-20,3852.67045263123 31 | -------------------------------------------------------------------------------- /tests/samples/30-4.csv: -------------------------------------------------------------------------------- 1 | 2024-06-20,-2948.9985879249 2 | 2043-02-02,-639.849287859328 3 | 2027-11-05,480.504552011908 4 | 2043-08-18,1426.95125028298 5 | 2041-01-26,483.612184678922 6 | 2041-04-11,20.40465018146 7 | 2043-11-11,-1313.52403883675 8 | 2037-07-26,-2094.65711499167 9 | 2029-05-08,-121.835290207001 10 | 2034-02-03,3232.86463043552 11 | 2026-11-18,622.983838739961 12 | 2032-11-12,-104.503915065111 13 | 2033-12-08,781.940620536238 14 | 2039-12-02,2860.55928353108 15 | 2041-06-26,723.549330249921 16 | 2030-02-26,-1323.63122768249 17 | 2024-12-09,-279.881239937948 18 | 2032-10-16,3619.86092006639 19 | 2031-11-29,-837.325559300572 20 | 2041-03-27,3560.10249478543 21 | 2041-05-03,2911.83031601581 22 | 2031-05-22,759.24930495407 23 | 2039-05-04,2900.32055170899 24 | 2041-01-17,-842.862760867145 25 | 2027-04-01,384.322720997276 26 | 2031-07-05,1235.46400872621 27 | 2041-04-28,1792.65989020192 28 | 2042-02-20,-3103.12289198329 29 | 2031-02-12,-118.870259495913 30 | 2036-09-26,-1548.01890774117 31 | -------------------------------------------------------------------------------- /tests/samples/30-40.csv: -------------------------------------------------------------------------------- 1 | 2028-08-17,-86.604887130168 2 | 2038-05-05,-3200.86043393609 3 | 2044-05-03,682.981969803743 4 | 2032-11-04,445.990710606358 5 | 2033-11-10,-1874.79842575144 6 | 2036-02-29,-45.305162919474 7 | 2034-07-08,-1434.47428230435 8 | 2046-05-29,-3020.45274878805 9 | 2038-03-02,-209.982953707738 10 | 2043-08-30,-1922.33009735981 11 | 2033-08-06,-337.953729624376 12 | 2031-06-03,-301.947647457077 13 | 2039-01-16,-130.166127533115 14 | 2045-05-03,-1037.53373611484 15 | 2038-05-11,511.874839664655 16 | 2036-10-16,1527.62894274377 17 | 2034-01-30,-973.155213661677 18 | 2033-06-03,1018.48755638198 19 | 2043-08-03,668.569550690272 20 | 2033-05-01,-159.596300567887 21 | 2028-11-19,-323.095545916332 22 | 2031-03-08,1405.28594266669 23 | 2044-03-07,118.942793955034 24 | 2032-01-14,352.606489950921 25 | 2043-10-31,-556.184620521873 26 | 2036-08-12,-3109.54877846031 27 | 2037-04-01,-1761.28861765792 28 | 2040-02-21,-3105.25754675052 29 | 2033-07-06,781.48766500223 30 | 2038-03-14,-2316.08299088103 31 | -------------------------------------------------------------------------------- /tests/samples/30-41.csv: -------------------------------------------------------------------------------- 1 | 2026-07-09,-1717.2246145223 2 | 2045-11-25,1762.32990210123 3 | 2041-10-08,95.28243914306 4 | 2036-10-29,-1751.09254632148 5 | 2032-11-27,1.863543371324 6 | 2027-07-28,-1804.86041108334 7 | 2030-11-12,-1506.78688158286 8 | 2043-01-13,190.973348111197 9 | 2030-02-02,-363.196249495219 10 | 2035-09-12,2807.26390051226 11 | 2028-11-16,-2714.37358521434 12 | 2036-09-07,-2842.71292206644 13 | 2027-08-13,1576.1648495402 14 | 2040-05-19,-3690.77327487533 15 | 2032-08-21,2920.21899210898 16 | 2045-01-27,270.169757896765 17 | 2041-07-24,-926.294464027925 18 | 2027-08-12,-1882.59598405283 19 | 2041-08-14,724.245009701947 20 | 2042-12-12,2929.54275950664 21 | 2039-06-15,-2366.05664918552 22 | 2036-03-19,745.612878909722 23 | 2031-09-29,-2511.69410635346 24 | 2030-05-14,474.68873195326 25 | 2042-04-13,-1073.88609927776 26 | 2041-03-06,1085.56780559676 27 | 2036-11-23,-326.034946444532 28 | 2029-11-27,-1885.58938698587 29 | 2038-11-28,2286.68273047645 30 | 2030-03-01,-3181.839970005 31 | -------------------------------------------------------------------------------- /tests/samples/30-42.csv: -------------------------------------------------------------------------------- 1 | 2025-12-26,-2040.32378185287 2 | 2027-05-19,-542.599465558309 3 | 2036-07-02,-1638.07921785046 4 | 2041-04-04,-1785.4348320952 5 | 2028-12-24,1423.2181987211 6 | 2043-07-28,-1404.85487574273 7 | 2042-09-11,-425.013058655437 8 | 2033-07-08,-770.928262745927 9 | 2036-04-14,-30.126815646033 10 | 2033-01-11,-137.497990202616 11 | 2040-10-24,-1598.48058898264 12 | 2044-10-06,-548.607016722692 13 | 2031-04-28,-1365.30144087584 14 | 2043-02-06,-552.729601883234 15 | 2033-12-31,-352.619333442496 16 | 2042-02-05,1024.8094464079 17 | 2026-07-18,530.984182161435 18 | 2033-10-23,107.491574805165 19 | 2029-05-30,998.090846034898 20 | 2026-07-29,742.867503051951 21 | 2042-07-20,514.88316746844 22 | 2029-08-12,685.358478474308 23 | 2042-09-23,-101.788658511194 24 | 2041-04-08,910.224575571443 25 | 2033-12-09,915.170596527048 26 | 2043-08-13,-318.87883096737 27 | 2029-05-19,-2298.63337858903 28 | 2031-09-07,193.152358582287 29 | 2029-12-02,-2908.88559006382 30 | 2030-11-29,-700.908582558304 31 | -------------------------------------------------------------------------------- /tests/samples/30-43.csv: -------------------------------------------------------------------------------- 1 | 2022-02-11,2291.04526134227 2 | 2037-03-14,3731.15382132256 3 | 2039-02-27,-18.870322044604 4 | 2038-01-22,1092.77936090495 5 | 2022-12-14,1794.66180411758 6 | 2029-02-13,1332.0856069124 7 | 2032-12-04,1828.93831365998 8 | 2028-12-04,4235.99996392663 9 | 2036-01-17,1863.34856318744 10 | 2025-01-19,1016.27499312779 11 | 2032-02-17,-4057.97375216177 12 | 2029-04-13,-339.994259110047 13 | 2029-09-06,1014.92572439913 14 | 2033-08-27,1533.60676036045 15 | 2041-11-23,-1068.38996674061 16 | 2041-04-21,-2740.21398703156 17 | 2024-01-29,2079.80226871767 18 | 2032-08-15,2252.97603019924 19 | 2026-10-19,2475.09961334318 20 | 2027-04-03,2114.6906841101 21 | 2040-11-30,-1384.74626407235 22 | 2025-11-02,904.406085432773 23 | 2034-12-23,570.144665719315 24 | 2039-06-04,-986.33587255377 25 | 2023-12-24,332.937328138593 26 | 2033-10-07,93.055793305046 27 | 2033-06-11,-3175.38791561091 28 | 2030-05-02,34.398005469563 29 | 2023-06-05,-968.246105817657 30 | 2024-09-04,-590.116816701308 31 | -------------------------------------------------------------------------------- /tests/samples/30-44.csv: -------------------------------------------------------------------------------- 1 | 2023-11-13,-1442.34825595749 2 | 2032-06-13,623.929174072353 3 | 2042-10-13,-254.956039723493 4 | 2039-09-23,2488.45696450003 5 | 2032-07-21,-3579.98739323925 6 | 2034-04-15,976.952435284897 7 | 2025-08-17,-2039.75978761114 8 | 2038-10-30,-839.799995319491 9 | 2042-12-01,-4782.04043400009 10 | 2037-08-06,-1855.07596316148 11 | 2024-11-30,-306.166024063965 12 | 2025-08-22,105.21321615149 13 | 2041-11-20,3743.80945820117 14 | 2032-08-20,4418.43656725342 15 | 2028-09-04,-1588.37761445423 16 | 2030-06-27,2628.25914101534 17 | 2041-05-17,870.331153853533 18 | 2026-05-17,-945.097434346666 19 | 2024-02-18,-2855.05510144858 20 | 2035-02-16,3604.64224034256 21 | 2027-03-19,-1439.33255546096 22 | 2030-06-14,-1318.87302940513 23 | 2041-09-24,808.535749281772 24 | 2041-01-22,1541.62805127589 25 | 2040-05-10,1861.08407064844 26 | 2028-08-12,-2269.42331735701 27 | 2032-08-14,19.494351547842 28 | 2038-08-19,-2356.68669034449 29 | 2040-04-12,-3493.15208446373 30 | 2041-06-18,495.818361074648 31 | -------------------------------------------------------------------------------- /tests/samples/30-45.csv: -------------------------------------------------------------------------------- 1 | 2028-12-03,-2989.18092600128 2 | 2033-04-08,9.331409260417 3 | 2035-12-14,-1626.82110676724 4 | 2031-10-03,-12.247514248105 5 | 2038-08-28,1255.21557311694 6 | 2034-02-13,1767.9226186556 7 | 2046-07-02,-156.902453121631 8 | 2034-06-26,-696.356327532919 9 | 2047-12-27,186.686257124797 10 | 2030-07-30,-1416.89666518073 11 | 2042-08-06,704.080858187278 12 | 2035-10-16,-1460.44822357104 13 | 2043-05-21,-210.848494440656 14 | 2044-06-27,3.008441300455 15 | 2042-03-06,-1803.38886722757 16 | 2036-09-20,561.116037337298 17 | 2033-06-06,143.802409840249 18 | 2039-11-25,-104.73793241245 19 | 2042-01-23,1161.78340979295 20 | 2043-12-07,-556.766946133021 21 | 2030-09-11,814.516059409505 22 | 2038-01-09,-571.832875433481 23 | 2035-07-10,-26.570597161002 24 | 2047-04-01,-644.687426613435 25 | 2033-11-23,-176.690611729568 26 | 2030-01-05,-4567.41848226181 27 | 2044-11-04,-1032.70886344985 28 | 2045-11-14,-360.75748628324 29 | 2029-03-04,-227.237376928569 30 | 2030-04-18,691.968611791618 31 | -------------------------------------------------------------------------------- /tests/samples/30-46.csv: -------------------------------------------------------------------------------- 1 | 2029-05-21,376.551230424907 2 | 2040-12-19,3665.48267688876 3 | 2042-07-15,160.023755881942 4 | 2036-11-08,1364.3524353882 5 | 2038-06-01,1819.89763586388 6 | 2033-09-23,243.801151742938 7 | 2041-01-22,282.824944470501 8 | 2047-02-12,-1649.10007461173 9 | 2048-10-16,-998.037809474325 10 | 2039-08-06,1306.15396443516 11 | 2043-07-18,-4876.48064834966 12 | 2029-09-09,361.305852497214 13 | 2047-11-12,-1545.37832697948 14 | 2030-02-12,1071.46671118985 15 | 2036-01-10,-773.890568329148 16 | 2045-04-07,-3113.09044528211 17 | 2044-03-13,-165.357824273015 18 | 2033-10-24,-2679.37324191378 19 | 2044-07-19,-1955.12290455103 20 | 2038-05-30,377.243354178301 21 | 2043-03-25,-89.006635151565 22 | 2042-07-13,309.449821183208 23 | 2036-08-26,2380.00672948439 24 | 2046-10-26,1324.6912328573 25 | 2047-08-21,727.562578103248 26 | 2043-08-15,2719.34018974277 27 | 2029-06-26,-17.099682798304 28 | 2047-07-08,1294.11598612724 29 | 2033-06-26,-150.904498972625 30 | 2046-11-19,682.862470994545 31 | -------------------------------------------------------------------------------- /tests/samples/30-47.csv: -------------------------------------------------------------------------------- 1 | 2026-12-05,-204.573325648394 2 | 2045-01-26,2262.32226088512 3 | 2037-08-04,372.645722063453 4 | 2029-11-09,-305.145254144891 5 | 2033-02-06,168.22630038343 6 | 2039-12-14,-91.856055022883 7 | 2045-04-03,-200.39983555764 8 | 2040-11-20,1879.56916657209 9 | 2040-12-08,-61.837575747517 10 | 2033-01-13,73.857233184105 11 | 2035-06-25,1108.3388553073 12 | 2039-06-09,1022.12706268439 13 | 2039-02-27,176.162564569314 14 | 2044-05-03,-2120.45775364122 15 | 2028-06-29,-1051.63999042528 16 | 2043-09-29,-2995.19526269172 17 | 2043-08-28,-1783.14286280994 18 | 2036-07-29,-3019.34153193515 19 | 2037-12-25,-59.130376320687 20 | 2039-11-24,-584.798880208237 21 | 2043-11-22,-989.684897181264 22 | 2044-12-07,960.29098664025 23 | 2039-02-23,-2020.87454618898 24 | 2029-03-12,-203.427703937411 25 | 2033-04-14,74.07498309004 26 | 2041-07-18,-3277.37148512559 27 | 2040-08-27,-3936.68004917426 28 | 2039-08-28,3009.53744122826 29 | 2042-06-05,-275.50581813146 30 | 2028-07-04,1354.0236830512 31 | -------------------------------------------------------------------------------- /tests/samples/30-48.csv: -------------------------------------------------------------------------------- 1 | 2023-10-16,-392.416796362717 2 | 2036-11-02,979.295211128165 3 | 2040-02-19,-7.285648077541 4 | 2030-07-04,-120.139044644889 5 | 2028-02-07,-560.389313101267 6 | 2033-08-10,616.933133224319 7 | 2024-11-28,175.419612889302 8 | 2032-12-21,1795.7948814247 9 | 2028-09-08,-2064.80623726193 10 | 2026-02-17,-2120.08784357186 11 | 2023-11-08,-854.762098787391 12 | 2041-02-11,663.3569962185 13 | 2025-12-10,350.650132362546 14 | 2023-12-22,17.79009809078 15 | 2036-12-10,-1763.31631843023 16 | 2026-12-12,-721.276887776513 17 | 2025-12-14,83.039251796241 18 | 2038-05-08,2211.59913882847 19 | 2041-09-26,-803.624684323083 20 | 2033-02-11,-2273.34657150641 21 | 2038-12-02,447.94811566584 22 | 2029-10-23,-1012.36985587599 23 | 2042-05-21,298.407974399472 24 | 2025-05-23,-2231.26049072134 25 | 2024-11-27,768.844873489684 26 | 2032-09-02,-702.215768202321 27 | 2034-02-08,-724.903576519865 28 | 2040-09-21,450.820374628256 29 | 2042-09-24,34.072968732318 30 | 2034-07-30,2364.79669283641 31 | -------------------------------------------------------------------------------- /tests/samples/30-5.csv: -------------------------------------------------------------------------------- 1 | 2021-02-12,-41.245801551985 2 | 2036-01-04,-1477.81439837976 3 | 2023-03-18,1160.25055576022 4 | 2035-03-11,-1946.14133207734 5 | 2026-05-19,-270.655612846399 6 | 2039-12-15,238.97923354416 7 | 2026-11-26,579.538533163087 8 | 2032-07-30,-3207.21006578276 9 | 2038-07-26,-1191.4887045293 10 | 2028-02-29,-66.299380328155 11 | 2027-12-27,775.03974884634 12 | 2024-04-06,4.465997009716 13 | 2031-07-10,-1615.945569325 14 | 2029-10-15,227.722105996984 15 | 2036-06-08,-251.117144215145 16 | 2039-11-13,-950.229418917574 17 | 2032-06-01,3815.50100326457 18 | 2029-04-10,93.275503077745 19 | 2021-02-21,1680.78910653696 20 | 2038-12-23,-2071.42394043698 21 | 2033-03-01,-618.773055350685 22 | 2029-04-17,291.964503857323 23 | 2024-04-02,-1909.41814055634 24 | 2034-07-26,-1328.15145892101 25 | 2022-02-07,929.748592344773 26 | 2023-10-03,2468.8250244153 27 | 2022-03-12,2430.52119317341 28 | 2024-09-07,169.775920871004 29 | 2031-10-09,716.777002586466 30 | 2027-02-06,271.212136653265 31 | -------------------------------------------------------------------------------- /tests/samples/30-6.csv: -------------------------------------------------------------------------------- 1 | 2023-02-02,1235.85913009861 2 | 2026-01-12,-1448.95718965467 3 | 2023-05-30,917.092589515829 4 | 2025-12-27,124.68659746294 5 | 2034-12-18,623.63908444579 6 | 2037-10-29,-3492.55035072709 7 | 2039-12-17,-1741.99130579261 8 | 2036-11-10,795.060578335852 9 | 2036-04-22,-812.591614029187 10 | 2034-02-12,-1201.19862139405 11 | 2037-03-24,-594.737087336321 12 | 2032-03-06,168.864209435341 13 | 2030-11-13,329.378673887443 14 | 2034-01-11,-526.157686531706 15 | 2029-07-10,-2400.13392560739 16 | 2030-07-11,-37.577630061256 17 | 2028-07-18,-316.467548684544 18 | 2033-05-08,1193.37502381764 19 | 2040-11-10,-2579.6073714617 20 | 2032-12-22,-3465.75719806609 21 | 2030-11-07,-24.36136075499 22 | 2024-11-06,562.908838762431 23 | 2024-03-26,-195.793028113729 24 | 2026-10-26,-1956.39778220728 25 | 2025-06-15,1535.24207375093 26 | 2040-04-08,1000.61091988922 27 | 2029-06-17,-61.380824083082 28 | 2025-10-01,-1605.9445892693 29 | 2032-01-04,-3942.59302111842 30 | 2042-11-07,-661.687477843952 31 | -------------------------------------------------------------------------------- /tests/samples/30-7.csv: -------------------------------------------------------------------------------- 1 | 2029-02-03,-291.311379664019 2 | 2035-05-21,3443.10621702129 3 | 2040-11-19,-810.84628790112 4 | 2045-10-28,1705.3404511493 5 | 2033-10-18,1123.46682908302 6 | 2037-06-04,-962.797841799478 7 | 2037-04-22,3141.58284655423 8 | 2031-07-16,1103.05094797842 9 | 2029-07-27,4033.95596787441 10 | 2045-09-04,1170.02876162166 11 | 2033-11-04,-988.094081918112 12 | 2029-02-16,-2442.72164893549 13 | 2048-11-20,-1564.0725985721 14 | 2044-11-02,572.535609226399 15 | 2030-04-28,-61.16146998232 16 | 2036-09-29,2587.69144544467 17 | 2033-05-29,1379.1360486064 18 | 2031-11-27,-1041.22579523404 19 | 2041-10-02,1416.67540003747 20 | 2032-05-19,-3140.48031652986 21 | 2037-08-11,537.347535081103 22 | 2030-10-30,29.860185799867 23 | 2031-11-03,-90.45772951246 24 | 2035-03-31,-507.57784978766 25 | 2029-02-14,-1081.1603792169 26 | 2048-01-01,-22.136964636402 27 | 2037-05-07,2206.17081647509 28 | 2048-07-26,504.957403906778 29 | 2048-08-25,-103.121081553895 30 | 2039-08-02,-2839.91464234067 31 | -------------------------------------------------------------------------------- /tests/samples/30-8.csv: -------------------------------------------------------------------------------- 1 | 2027-05-19,-25.344505063982 2 | 2037-07-02,773.747045134311 3 | 2046-05-01,-386.999664629661 4 | 2041-11-13,419.818434140565 5 | 2045-10-30,-2862.39673644965 6 | 2028-09-07,-1236.68009731454 7 | 2043-02-11,-1361.49085550695 8 | 2044-11-17,-1952.23013291851 9 | 2036-09-03,3861.98683353189 10 | 2036-04-26,1390.96500532992 11 | 2041-10-16,-4.849935469875 12 | 2037-03-25,596.764736001767 13 | 2029-10-25,2188.07393663027 14 | 2045-02-17,850.720491281197 15 | 2034-05-02,-1281.85619870839 16 | 2035-03-06,465.228152147822 17 | 2046-09-07,155.401889648259 18 | 2031-04-14,64.132599380048 19 | 2034-01-31,2332.46293518395 20 | 2027-11-15,118.562882418809 21 | 2028-07-09,526.844642981429 22 | 2031-10-15,292.848795523599 23 | 2042-10-09,2591.75750733989 24 | 2038-04-21,-3504.54110812878 25 | 2041-01-14,1110.2001079432 26 | 2029-04-04,-168.795939782797 27 | 2040-10-25,-1232.41598117654 28 | 2039-07-17,2573.06189195922 29 | 2040-03-07,12.612885004022 30 | 2041-06-09,-453.413496594769 31 | -------------------------------------------------------------------------------- /tests/samples/30-9.csv: -------------------------------------------------------------------------------- 1 | 2028-08-23,-1154.21337109978 2 | 2040-09-12,428.670627368136 3 | 2045-05-06,752.735876449248 4 | 2041-02-04,-3.224555318693 5 | 2037-06-19,1517.89458578291 6 | 2035-01-05,-3826.25223970592 7 | 2044-09-21,1108.78677476988 8 | 2041-09-10,3518.64405319768 9 | 2040-10-05,-1203.64272019251 10 | 2041-09-11,-114.194225962632 11 | 2043-02-09,2200.7457392405 12 | 2041-07-26,2455.09012288706 13 | 2031-11-29,9.620594310957 14 | 2037-03-23,954.957016274741 15 | 2036-02-28,-1623.83263707404 16 | 2032-05-09,-1442.76934776142 17 | 2042-08-28,414.109393729968 18 | 2032-01-20,1698.39390979567 19 | 2030-03-03,3850.1009427366 20 | 2033-02-10,-2614.50668927086 21 | 2036-01-19,-1707.96110148736 22 | 2036-07-03,-112.71523539016 23 | 2039-09-01,-317.904278490399 24 | 2042-01-11,-1637.90903460935 25 | 2033-11-08,2410.20376088294 26 | 2029-05-23,1832.31940733189 27 | 2044-11-02,41.823112819506 28 | 2039-02-10,2210.75097744568 29 | 2042-10-24,-931.950316674173 30 | 2033-08-06,89.714756592142 31 | -------------------------------------------------------------------------------- /tests/samples/minus_0_13.csv: -------------------------------------------------------------------------------- 1 | 2015-05-04,-200.00 2 | 2015-05-07,-75.00 3 | 2015-05-15,-620.00 4 | 2015-05-04,-0.01 5 | 2015-05-06,-100.00 6 | 2015-05-14,-299.99 7 | 2015-05-19,-20.00 8 | 2015-05-26,-100.00 9 | 2015-05-27,-340.00 10 | 2015-06-01,-500.00 11 | 2015-06-05,-100.00 12 | 2015-06-18,-1008.52 13 | 2015-06-22,-65.00 14 | 2015-06-22,-120.00 15 | 2015-06-22,-450.00 16 | 2015-06-30,-450.75 17 | 2015-07-06,-80.00 18 | 2015-07-13,-130.00 19 | 2015-07-16,-1050.00 20 | 2015-07-31,-1000.00 21 | 2015-08-10,-65.00 22 | 2015-08-17,-400.00 23 | 2015-09-14,-100.00 24 | 2015-09-16,-956.88 25 | 2015-09-28,-117.00 26 | 2015-10-01,-50.00 27 | 2015-10-02,-1130.00 28 | 2015-10-29,-70.00 29 | 2015-11-02,-188 30 | 2015-11-06,-250 31 | 2015-12-29,-100 32 | 2015-12-31,9518.08 33 | -------------------------------------------------------------------------------- /tests/samples/minus_0_99.csv: -------------------------------------------------------------------------------- 1 | 2021-06-09,-134.09 2 | 2021-08-11,40.86 3 | -------------------------------------------------------------------------------- /tests/samples/minus_0_993.csv: -------------------------------------------------------------------------------- 1 | 2021-06-08,-1000000.0 2 | 2022-06-29,5000.0 3 | 2023-06-08,-3.0 4 | 2023-06-30,0.0 -------------------------------------------------------------------------------- /tests/samples/minus_0_99999.csv: -------------------------------------------------------------------------------- 1 | 2020-03-05,-18480.0 2 | 2020-03-16,13120.0 3 | -------------------------------------------------------------------------------- /tests/samples/random_100.csv: -------------------------------------------------------------------------------- 1 | 2020-01-01,1426.4559062157832 2 | 2020-01-02,-4457.236863629762 3 | 2020-01-03,-1507.6787582662078 4 | 2020-01-04,2979.4941460347945 5 | 2020-01-05,-4799.539021733898 6 | 2020-01-06,2535.0346055041127 7 | 2020-01-07,-2458.9778927417474 8 | 2020-01-08,4857.076521833611 9 | 2020-01-09,-504.45260882987714 10 | 2020-01-10,1009.8440088107773 11 | 2020-01-11,-1181.9194375530374 12 | 2020-01-12,195.96817523829122 13 | 2020-01-13,-2991.6701996272154 14 | 2020-01-14,-4000.4305431313437 15 | 2020-01-15,-1129.8115296062251 16 | 2020-01-16,-1196.1067382387191 17 | 2020-01-17,-3322.2569563277 18 | 2020-01-18,-276.4786286818653 19 | 2020-01-19,1495.2247201424389 20 | 2020-01-20,2697.8655170292277 21 | 2020-01-21,4597.712358050014 22 | 2020-01-22,-1620.783317506187 23 | 2020-01-23,-1652.6323380033646 24 | 2020-01-24,3826.0599416491095 25 | 2020-01-25,904.027861046663 26 | 2020-01-26,1000.3129139225375 27 | 2020-01-27,-2391.2688826305507 28 | 2020-01-28,3025.6020341598532 29 | 2020-01-29,659.1720097074895 30 | 2020-01-30,4924.99136077139 31 | 2020-01-31,-4519.180566979954 32 | 2020-02-01,-4679.05567898298 33 | 2020-02-02,-3154.4981386041445 34 | 2020-02-03,-104.69366192249436 35 | 2020-02-04,-3328.5966579601036 36 | 2020-02-05,-2404.857387310868 37 | 2020-02-06,-3058.4722689926666 38 | 2020-02-07,827.4977703536288 39 | 2020-02-08,4081.313722812358 40 | 2020-02-09,-547.5679867076396 41 | 2020-02-10,3962.742627198766 42 | 2020-02-11,1301.2850437431898 43 | 2020-02-12,-643.9406260236437 44 | 2020-02-13,-2390.929146645975 45 | 2020-02-14,-4470.949353373 46 | 2020-02-15,742.7400975099426 47 | 2020-02-16,-1270.5315574838082 48 | 2020-02-17,-48.679172570778064 49 | 2020-02-18,3309.6880134364055 50 | 2020-02-19,1747.8484819607856 51 | 2020-02-20,237.5888412260265 52 | 2020-02-21,-3142.3617098575587 53 | 2020-02-22,-2234.72283420558 54 | 2020-02-23,4029.2731669389323 55 | 2020-02-24,3640.6269952077364 56 | 2020-02-25,-40.70456984311113 57 | 2020-02-26,4397.457834049523 58 | 2020-02-27,312.7574444804868 59 | 2020-02-28,4779.598677323083 60 | 2020-02-29,483.73779666598966 61 | 2020-03-01,-753.7825932190335 62 | 2020-03-02,1086.436170630359 63 | 2020-03-03,-55.55327092546668 64 | 2020-03-04,4750.075093284335 65 | 2020-03-05,-940.066448436979 66 | 2020-03-06,-4140.43894953209 67 | 2020-03-07,3450.028885922069 68 | 2020-03-08,2754.5299042147926 69 | 2020-03-09,-2988.9815417855125 70 | 2020-03-10,4775.868548375582 71 | 2020-03-11,56.48199869302607 72 | 2020-03-12,-2890.5561820372504 73 | 2020-03-13,-337.6820754058981 74 | 2020-03-14,1096.7154761969323 75 | 2020-03-15,-1322.3726812602367 76 | 2020-03-16,430.60548653370824 77 | 2020-03-17,-4384.157287576673 78 | 2020-03-18,2345.5852188305353 79 | 2020-03-19,889.2026250023473 80 | 2020-03-20,-433.059950116678 81 | 2020-03-21,-3769.158798767545 82 | 2020-03-22,2794.530878706434 83 | 2020-03-23,2327.1571999989737 84 | 2020-03-24,-1331.3066960701035 85 | 2020-03-25,-4563.238405112265 86 | 2020-03-26,1640.9263219588684 87 | 2020-03-27,2960.51341317291 88 | 2020-03-28,-1789.6041184026403 89 | 2020-03-29,-941.3095185865318 90 | 2020-03-30,2043.7078195791446 91 | 2020-03-31,18.545572696873023 92 | 2020-04-01,3165.909138461837 93 | 2020-04-02,2166.9338000354774 94 | 2020-04-03,634.2563490922303 95 | 2020-04-04,1091.280863152644 96 | 2020-04-05,715.6713917925663 97 | 2020-04-06,420.7078556447732 98 | 2020-04-07,-2613.236520174589 99 | 2020-04-08,-354.86167124080475 100 | 2020-04-09,-1720.1226536088898 101 | -------------------------------------------------------------------------------- /tests/samples/rw-100.csv: -------------------------------------------------------------------------------- 1 | 2000-01-01,-1000000.0 2 | 2000-01-01,10365.378864363389 3 | 2000-01-31,14815.415470294169 4 | 2000-03-01,12030.562733418017 5 | 2000-03-31,11307.979942311524 6 | 2000-04-30,17393.026477395906 7 | 2000-05-30,10111.520084997483 8 | 2000-06-29,11777.760495889588 9 | 2000-07-29,9425.622181866325 10 | 2000-08-28,15628.377143997544 11 | 2000-09-27,9702.463005875723 12 | 2000-10-27,15856.679956400441 13 | 2000-11-26,7536.830552079986 14 | 2000-12-26,15610.748972849195 15 | 2001-01-25,12244.61630572664 16 | 2001-02-24,11440.499598220922 17 | 2001-03-26,16389.68889989676 18 | 2001-04-25,8924.054568318044 19 | 2001-05-25,14793.134925800208 20 | 2001-06-24,16036.87407471702 21 | 2001-07-24,11774.764247927264 22 | 2001-08-23,10148.729650154515 23 | 2001-09-22,15523.43345913048 24 | 2001-10-22,12968.672606115631 25 | 2001-11-21,13295.228164108466 26 | 2001-12-21,17234.0533865184 27 | 2002-01-20,12855.582301574757 28 | 2002-02-19,11332.650415145608 29 | 2002-03-21,10340.065865024482 30 | 2002-04-20,10809.698350285784 31 | 2002-05-20,9416.931526240587 32 | 2002-06-19,10980.441703375955 33 | 2002-07-19,10292.951072761207 34 | 2002-08-18,17249.589174226494 35 | 2002-09-17,12084.28998231004 36 | 2002-10-17,9315.538003932472 37 | 2002-11-16,8609.349472916037 38 | 2002-12-16,14691.770706542706 39 | 2003-01-15,8450.698171422924 40 | 2003-02-14,12948.035349790975 41 | 2003-03-16,13530.318420038606 42 | 2003-04-15,14538.323495605848 43 | 2003-05-15,12046.162449959194 44 | 2003-06-14,10832.319130654103 45 | 2003-07-14,16495.616055439532 46 | 2003-08-13,10676.879107776198 47 | 2003-09-12,12090.198592563082 48 | 2003-10-12,10646.566503179505 49 | 2003-11-11,10488.917821443225 50 | 2003-12-11,10821.398073651553 51 | 2004-01-10,13796.07016032582 52 | 2004-02-09,17230.011611409267 53 | 2004-03-10,11194.995563445955 54 | 2004-04-09,11814.34556830408 55 | 2004-05-09,13608.376888722712 56 | 2004-06-08,10594.307904257505 57 | 2004-07-08,10166.765732386435 58 | 2004-08-07,15833.016943050037 59 | 2004-09-06,9200.039765804853 60 | 2004-10-06,8350.800836539574 61 | 2004-11-05,15658.541973800531 62 | 2004-12-05,9509.214362555325 63 | 2005-01-04,14532.667944151059 64 | 2005-02-03,10309.61112254181 65 | 2005-03-05,10443.534156861997 66 | 2005-04-04,14842.312924625034 67 | 2005-05-04,15038.568489678159 68 | 2005-06-03,15255.677522253782 69 | 2005-07-03,16223.787124615072 70 | 2005-08-02,15192.052628098396 71 | 2005-09-01,8841.623001019849 72 | 2005-10-01,13192.253118052433 73 | 2005-10-31,12111.569405373712 74 | 2005-11-30,14887.156246485036 75 | 2005-12-30,12544.271309525768 76 | 2006-01-29,16024.132284414649 77 | 2006-02-28,16663.25391795901 78 | 2006-03-30,14377.096445477888 79 | 2006-04-29,10062.345942299968 80 | 2006-05-29,11771.472692254092 81 | 2006-06-28,17496.750504429547 82 | 2006-07-28,10988.45972418063 83 | 2006-08-27,17348.837398907228 84 | 2006-09-26,12367.24713060131 85 | 2006-10-26,9765.378568208846 86 | 2006-11-25,15891.886411452715 87 | 2006-12-25,12518.086311310873 88 | 2007-01-24,8509.677591501983 89 | 2007-02-23,8648.4031376079 90 | 2007-03-25,13248.069865865491 91 | 2007-04-24,15967.767234625975 92 | 2007-05-24,8475.297565914076 93 | 2007-06-23,16178.5238769798 94 | 2007-07-23,10091.194073369616 95 | 2007-08-22,10581.218629853609 96 | 2007-09-21,11733.279338775046 97 | 2007-10-21,12099.24135653874 98 | 2007-11-20,14814.894928569258 99 | 2007-12-20,13927.990551005641 100 | 2008-01-19,14406.157556095503 101 | 2008-02-18,16770.931116091037 102 | -------------------------------------------------------------------------------- /tests/samples/rw-50.csv: -------------------------------------------------------------------------------- 1 | 2000-01-01,-1000000.0 2 | 2000-01-01,20013.762767996534 3 | 2000-01-31,22627.142853733552 4 | 2000-03-01,21913.842208046477 5 | 2000-03-31,28551.419495848317 6 | 2000-04-30,19508.64312770872 7 | 2000-05-30,24018.508012029757 8 | 2000-06-29,24703.196702143276 9 | 2000-07-29,16350.81225543668 10 | 2000-08-28,25369.483686095657 11 | 2000-09-27,24090.660535131305 12 | 2000-10-27,15079.15134693606 13 | 2000-11-26,27241.963510804293 14 | 2000-12-26,30602.792879389785 15 | 2001-01-25,32405.738249846454 16 | 2001-02-24,16199.337003723545 17 | 2001-03-26,16843.52425324068 18 | 2001-04-25,19415.85805136018 19 | 2001-05-25,29658.82772067889 20 | 2001-06-24,30080.627353209547 21 | 2001-07-24,20217.67685581632 22 | 2001-08-23,29394.443032110572 23 | 2001-09-22,26093.583426970967 24 | 2001-10-22,20942.237132321323 25 | 2001-11-21,32096.997928524153 26 | 2001-12-21,16771.82267158162 27 | 2002-01-20,27451.535464227807 28 | 2002-02-19,22240.671748900044 29 | 2002-03-21,19693.20889017347 30 | 2002-04-20,15656.644171908643 31 | 2002-05-20,29140.80993974786 32 | 2002-06-19,33719.224409649316 33 | 2002-07-19,24093.858627480684 34 | 2002-08-18,31418.394007668012 35 | 2002-09-17,34369.59753185898 36 | 2002-10-17,22454.37408450286 37 | 2002-11-16,34298.04139895853 38 | 2002-12-16,31127.311743382554 39 | 2003-01-15,29771.166236615736 40 | 2003-02-14,23714.6047053632 41 | 2003-03-16,29201.63326709254 42 | 2003-04-15,33590.97562618985 43 | 2003-05-15,34885.21721090955 44 | 2003-06-14,21172.971845923967 45 | 2003-07-14,34398.16572113883 46 | 2003-08-13,33271.988707028024 47 | 2003-09-12,34491.42469951532 48 | 2003-10-12,25499.11783914676 49 | 2003-11-11,20585.927728707844 50 | 2003-12-11,30744.79592974533 51 | 2004-01-10,20570.335028059013 52 | -------------------------------------------------------------------------------- /tests/samples/unordered.csv: -------------------------------------------------------------------------------- 1 | 2015-06-11,-1000 2 | 2015-07-21,-9000 3 | 2018-06-10,20000 4 | 2015-10-17,-3000 5 | -------------------------------------------------------------------------------- /tests/samples/xnfv.csv: -------------------------------------------------------------------------------- 1 | "2011-11-30", -100000 2 | "2012-03-15", -50000 3 | "2012-07-18", -2500 4 | "2012-11-30", 12500 5 | "2013-01-23", 37500 6 | "2013-04-30", 75000 7 | "2014-02-06", 90000 8 | -------------------------------------------------------------------------------- /tests/samples/zeros.csv: -------------------------------------------------------------------------------- 1 | 2021-12-31,0 2 | 2022-12-31,0 3 | 2023-12-31,-100 4 | 2024-12-31,10 5 | 2025-12-31,0 6 | 2026-12-31,10 7 | 2027-12-31,10 8 | 2028-12-31,10 9 | 2029-12-31,200 10 | 2030-12-31,0 11 | 2031-12-31,0 12 | 2032-12-31,0 13 | 2033-12-31,0 14 | -------------------------------------------------------------------------------- /tests/test_conversion.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{exceptions, prelude::*, types::*}; 2 | use rstest::*; 3 | 4 | mod common; 5 | use common::{pd_read_csv, PaymentsLoader}; 6 | 7 | type Payments = (Py, Py); 8 | 9 | const INPUT: &str = "tests/samples/unordered.csv"; 10 | const EXPECTED: f64 = 0.16353715844; 11 | 12 | #[fixture] 13 | fn payments(#[default(INPUT)] input: &str) -> Payments { 14 | Python::with_gil(|py| { 15 | let (dates, amounts) = PaymentsLoader::from_csv(py, input).to_columns(); 16 | (dates.into(), amounts.into()) 17 | }) 18 | } 19 | 20 | fn get_locals<'p>(py: Python<'p>, extra_imports: Option<&[&str]>) -> &'p PyDict { 21 | let builtins = py.import("builtins").unwrap(); 22 | let data = payments(INPUT); 23 | let locals = py_dict!(py, "dates" => data.0, "amounts" => data.1, "__builtins__" => builtins); 24 | let locals = py_dict_merge!(py, locals, builtins.dict()); 25 | 26 | for &name in extra_imports.unwrap_or_default() { 27 | let module = py.import(name).unwrap_or_else(|_| panic!("{:?} is not installed", name)); 28 | locals.set_item(name, module).unwrap(); 29 | } 30 | 31 | locals 32 | } 33 | 34 | #[rstest] 35 | fn test_extract_from_iter() { 36 | let result: f64 = Python::with_gil(|py| { 37 | let locals = get_locals(py, Some(&["datetime"])); 38 | let dates_iter = py 39 | .eval("(datetime.datetime(x.year, x.month, x.day) for x in dates)", Some(locals), None) 40 | .unwrap(); 41 | let amounts_gen = py.eval("(x for x in amounts)", Some(locals), None).unwrap(); 42 | pyxirr_call!(py, "xirr", (dates_iter, amounts_gen)) 43 | }); 44 | 45 | assert_almost_eq!(result, EXPECTED); 46 | } 47 | 48 | #[rstest] 49 | fn test_extract_from_tuples(payments: Payments) { 50 | let result: f64 = Python::with_gil(|py| pyxirr_call!(py, "xirr", payments)); 51 | assert_almost_eq!(result, EXPECTED); 52 | } 53 | 54 | #[rstest] 55 | fn test_extract_from_lists() { 56 | let result: f64 = Python::with_gil(|py| { 57 | let locals = get_locals(py, None); 58 | let data = py.eval("map(list, zip(dates, amounts))", Some(locals), None).unwrap(); 59 | pyxirr_call!(py, "xirr", (data,)) 60 | }); 61 | assert_almost_eq!(result, EXPECTED); 62 | } 63 | 64 | #[rstest] 65 | fn test_extract_from_dict() { 66 | let input = "tests/samples/unordered.csv"; 67 | let result: f64 = Python::with_gil(|py| { 68 | let data = PaymentsLoader::from_csv(py, input).to_dict(); 69 | pyxirr_call!(py, "xirr", (data,)) 70 | }); 71 | assert_almost_eq!(result, EXPECTED); 72 | } 73 | 74 | #[rstest] 75 | fn test_extract_dates_from_strings() { 76 | Python::with_gil(|py| { 77 | let locals = get_locals(py, Some(&["datetime"])); 78 | let amounts = locals.get_item("amounts").unwrap(); 79 | 80 | // parse from %Y-%m-%d 81 | let dates_iter = 82 | py.eval("(x.strftime('%Y-%m-%d') for x in dates)", Some(locals), None).unwrap(); 83 | let result: f64 = pyxirr_call!(py, "xirr", (dates_iter, amounts)); 84 | assert_almost_eq!(result, EXPECTED); 85 | 86 | // parse from %m/%d/%Y 87 | let dates_iter = 88 | py.eval("(x.strftime('%m/%d/%Y') for x in dates)", Some(locals), None).unwrap(); 89 | let result: f64 = pyxirr_call!(py, "xirr", (dates_iter, amounts)); 90 | assert_almost_eq!(result, EXPECTED); 91 | 92 | // parse from datetime to %Y-%m-%d 93 | let dates_iter = py 94 | .eval("(x.strftime('%Y-%m-%dT12:30:08.483694') for x in dates)", Some(locals), None) 95 | .unwrap(); 96 | let result: f64 = pyxirr_call!(py, "xirr", (dates_iter, amounts)); 97 | assert_almost_eq!(result, EXPECTED); 98 | 99 | // unknown format 100 | let dates_iter = 101 | py.eval("(x.strftime('%d %b %y') for x in dates)", Some(locals), None).unwrap(); 102 | let err = pyxirr_call_impl!(py, "xirr", (dates_iter, amounts)).unwrap_err(); 103 | assert!(err.is_instance(py, py.get_type::())); 104 | }); 105 | } 106 | 107 | #[rstest] 108 | #[cfg_attr(feature = "nonumpy", ignore)] 109 | fn test_extract_from_numpy_object_array() { 110 | let result: f64 = Python::with_gil(|py| { 111 | let locals = get_locals(py, Some(&["numpy"])); 112 | let data = py.eval("numpy.array([dates, amounts])", Some(locals), None).unwrap(); 113 | pyxirr_call!(py, "xirr", (data,)) 114 | }); 115 | 116 | assert_almost_eq!(result, EXPECTED); 117 | } 118 | 119 | #[rstest] 120 | #[cfg_attr(feature = "nonumpy", ignore)] 121 | fn test_extract_from_numpy_arrays() { 122 | let result: f64 = Python::with_gil(|py| { 123 | let locals = get_locals(py, Some(&["numpy"])); 124 | let dates = 125 | py.eval("numpy.array(dates, dtype='datetime64[D]')", Some(locals), None).unwrap(); 126 | let amounts = py.eval("numpy.array(amounts)", Some(locals), None).unwrap(); 127 | pyxirr_call!(py, "xirr", (dates, amounts)) 128 | }); 129 | 130 | assert_almost_eq!(result, EXPECTED); 131 | } 132 | 133 | #[rstest] 134 | #[cfg_attr(feature = "nonumpy", ignore)] 135 | fn test_extract_from_pandas_dataframe() { 136 | let result: f64 = Python::with_gil(|py| { 137 | let data = pd_read_csv(py, INPUT); 138 | pyxirr_call!(py, "xirr", (data,)) 139 | }); 140 | 141 | assert_almost_eq!(result, EXPECTED); 142 | } 143 | 144 | #[rstest] 145 | #[cfg_attr(feature = "nonumpy", ignore)] 146 | fn test_extract_from_pandas_series() { 147 | let result: f64 = Python::with_gil(|py| { 148 | let locals = get_locals(py, Some(&["pandas"])); 149 | let dates = py.eval("pandas.Series(dates)", Some(locals), None).unwrap(); 150 | let amounts = py.eval("pandas.Series(amounts)", Some(locals), None).unwrap(); 151 | pyxirr_call!(py, "xirr", (dates, amounts)) 152 | }); 153 | 154 | assert_almost_eq!(result, EXPECTED); 155 | } 156 | 157 | #[rstest] 158 | #[cfg_attr(feature = "nonumpy", ignore)] 159 | fn test_extract_from_pandas_series_with_datetime_index() { 160 | let result: f64 = Python::with_gil(|py| { 161 | let locals = get_locals(py, Some(&["pandas"])); 162 | let dates = py 163 | .eval("pandas.Series(amounts, index=pandas.to_datetime(dates))", Some(locals), None) 164 | .unwrap(); 165 | pyxirr_call!(py, "xirr", (dates,)) 166 | }); 167 | 168 | assert_almost_eq!(result, EXPECTED); 169 | } 170 | 171 | #[rstest] 172 | #[cfg_attr(feature = "nonumpy", ignore)] 173 | fn test_failed_extract_from_pandas_series_with_int64_index() { 174 | Python::with_gil(|py| { 175 | let locals = get_locals(py, Some(&["pandas"])); 176 | let dates = py.eval("pandas.Series(amounts)", Some(locals), None).unwrap(); 177 | let err = pyxirr_call_impl!(py, "xirr", (dates,)).unwrap_err(); 178 | assert!(err.is_instance(py, py.get_type::())); 179 | }); 180 | } 181 | 182 | #[rstest] 183 | #[cfg_attr(feature = "nonumpy", ignore)] 184 | fn test_extract_from_mixed_iterables() { 185 | let result: f64 = Python::with_gil(|py| { 186 | let locals = get_locals(py, Some(&["pandas", "numpy"])); 187 | let dates = py.eval("map(pandas.Timestamp, dates)", Some(locals), None).unwrap(); 188 | let amounts = py.eval("numpy.array(amounts)", Some(locals), None).unwrap(); 189 | pyxirr_call!(py, "xirr", (dates, amounts)) 190 | }); 191 | 192 | assert_almost_eq!(result, EXPECTED); 193 | } 194 | 195 | #[rstest] 196 | fn test_extract_from_non_float() { 197 | Python::with_gil(|py| { 198 | let locals = get_locals(py, Some(&["decimal"])); 199 | let dates = locals.get_item("dates").unwrap(); 200 | 201 | let amounts = py.eval("map(decimal.Decimal, amounts)", Some(locals), None).unwrap(); 202 | let result: f64 = pyxirr_call!(py, "xirr", (dates, amounts)); 203 | assert_almost_eq!(result, EXPECTED); 204 | 205 | let amounts = py.eval("map(int, amounts)", Some(locals), None).unwrap(); 206 | let result: f64 = pyxirr_call!(py, "xirr", (dates, amounts)); 207 | assert_almost_eq!(result, EXPECTED); 208 | 209 | let amounts = py.eval("map(str, amounts)", Some(locals), None).unwrap(); 210 | let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err(); 211 | assert!(err.is_instance(py, py.get_type::())); 212 | }) 213 | } 214 | 215 | #[rstest] 216 | fn test_payments_different_sign() { 217 | Python::with_gil(|py| { 218 | let locals = get_locals(py, None); 219 | let dates = locals.get_item("dates").unwrap(); 220 | 221 | let amounts = py.eval("(abs(x) for x in amounts)", Some(locals), None).unwrap(); 222 | let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err(); 223 | assert!(err.is_instance(py, py.get_type::())); 224 | 225 | let amounts = py.eval("(-abs(x) for x in amounts)", Some(locals), None).unwrap(); 226 | let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err(); 227 | assert!(err.is_instance(py, py.get_type::())); 228 | }) 229 | } 230 | 231 | #[rstest] 232 | fn test_arrays_of_dirrerent_lengths() { 233 | Python::with_gil(|py| { 234 | let locals = get_locals(py, None); 235 | let dates = locals.get_item("dates").unwrap(); 236 | let amounts = py.eval("amounts[:-2]", Some(locals), None).unwrap(); 237 | let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err(); 238 | assert!(err.is_instance(py, py.get_type::())); 239 | }) 240 | } 241 | -------------------------------------------------------------------------------- /tests/test_scheduled.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{ 2 | types::{PyDate, PyList}, 3 | IntoPy, PyResult, Python, 4 | }; 5 | use rstest::rstest; 6 | 7 | mod common; 8 | use common::PaymentsLoader; 9 | 10 | #[rstest] 11 | #[case::unordered("tests/samples/unordered.csv", 2218.42566365675)] 12 | #[case::random_100("tests/samples/random_100.csv", 6488.0382272781)] 13 | #[case::random_1000("tests/samples/random_1000.csv", 41169.6659983284)] 14 | fn test_xnpv_samples(#[case] input: &str, #[case] expected: f64) { 15 | let rate = 0.1; 16 | let result: f64 = Python::with_gil(|py| { 17 | let payments = PaymentsLoader::from_csv(py, input).to_records(); 18 | pyxirr_call!(py, "xnpv", (rate, payments)) 19 | }); 20 | assert_almost_eq!(result, expected); 21 | } 22 | 23 | #[rstest] 24 | #[case::unordered("tests/samples/unordered.csv", 0.16353715844)] 25 | #[case::single_redemption("tests/samples/single_redemption.csv", 0.13616957937417506)] 26 | #[case::random("tests/samples/random.csv", 0.6924974337277426)] 27 | #[case::random_100("tests/samples/random_100.csv", 29.829404437653)] 28 | #[case::random_1000("tests/samples/random_1000.csv", 5.508930558032)] 29 | #[case::case_30_0("tests/samples/30-0.csv", 0.1660454339589889)] 30 | #[case::case_30_1("tests/samples/30-1.csv", 0.18180763138335373)] 31 | #[case::case_30_2("tests/samples/30-2.csv", -0.0027489547855574564)] 32 | #[case::case_30_3("tests/samples/30-3.csv", 5.852451769434373)] 33 | #[case::case_30_4("tests/samples/30-4.csv", 0.16098047379438984)] 34 | #[case::case_30_5("tests/samples/30-5.csv", 0.008979287890185613)] 35 | #[case::case_30_6("tests/samples/30-6.csv", 0.3255467341810659)] 36 | #[case::case_30_7("tests/samples/30-7.csv", 0.3501464865493174)] 37 | #[case::case_30_8("tests/samples/30-8.csv", 3.353029509425298)] 38 | #[case::case_30_9("tests/samples/30-9.csv", 2.878013825163697)] 39 | #[case::case_30_10("tests/samples/30-10.csv", 0.11143674454788119)] 40 | #[case::case_30_11("tests/samples/30-11.csv", -0.12606921657689435)] 41 | #[case::case_30_12("tests/samples/30-12.csv", -0.02578630164755525)] 42 | #[case::case_30_13("tests/samples/30-13.csv", -0.6590570693637554)] // -0.02910731236366771 43 | #[case::case_30_14("tests/samples/30-14.csv", 0.6996860198137344)] 44 | #[case::case_30_15("tests/samples/30-15.csv", 0.02976853488940409)] 45 | #[case::case_30_16("tests/samples/30-16.csv", 0.44203743561153225)] 46 | #[case::case_30_17("tests/samples/30-17.csv", 2.7956075643765765)] 47 | #[case::case_30_18("tests/samples/30-18.csv", -0.2692266976014054)] 48 | #[case::case_30_19("tests/samples/30-19.csv", -0.0016474932646633118)] 49 | #[case::case_30_20("tests/samples/30-20.csv", f64::NAN)] 50 | #[case::case_30_21("tests/samples/30-21.csv", 0.05900900202336096)] 51 | #[case::case_30_22("tests/samples/30-22.csv", -0.028668460065440993)] // -0.3154082674273421 52 | #[case::case_30_23("tests/samples/30-23.csv", 1.1276768367328942)] 53 | #[case::case_30_24("tests/samples/30-24.csv", 32.90894421344702)] 54 | #[case::case_30_25("tests/samples/30-25.csv", -0.001245880387491199)] 55 | #[case::case_30_26("tests/samples/30-26.csv", -0.33228389267806224)] 56 | #[case::case_30_27("tests/samples/30-27.csv", 0.00017475536849502265)] 57 | #[case::case_30_28("tests/samples/30-28.csv", -0.10396735360664396)] // 1.1258287638216773 58 | #[case::case_30_29("tests/samples/30-29.csv", f64::NAN)] 59 | #[case::case_30_30("tests/samples/30-30.csv", 0.08115488395163964)] 60 | #[case::case_30_31("tests/samples/30-31.csv", f64::NAN)] 61 | #[case::case_30_32("tests/samples/30-32.csv", -0.1305850720162)] 62 | #[case::case_30_33("tests/samples/30-33.csv", f64::NAN)] 63 | #[case::case_30_34("tests/samples/30-34.csv", f64::NAN)] 64 | #[case::case_30_35("tests/samples/30-35.csv", -0.23061428300107065)] 65 | #[case::case_30_36("tests/samples/30-36.csv", -0.09610929159865819)] 66 | #[case::case_30_37("tests/samples/30-37.csv", -0.17219174455291367)] // -0.6519313380797903 67 | #[case::case_30_38("tests/samples/30-38.csv", f64::NAN)] 68 | #[case::case_30_39("tests/samples/30-39.csv", -0.202699788567102)] 69 | #[case::case_30_40("tests/samples/30-40.csv", f64::NAN)] 70 | #[case::case_30_41("tests/samples/30-41.csv", -0.11644766662933352)] 71 | #[case::case_30_42("tests/samples/30-42.csv", f64::NAN)] 72 | #[case::case_30_43("tests/samples/30-43.csv", -0.12837518345271245)] 73 | #[case::case_30_44("tests/samples/30-44.csv", f64::NAN)] 74 | #[case::case_30_45("tests/samples/30-45.csv", f64::NAN)] 75 | #[case::case_30_46("tests/samples/30-46.csv", -0.047401670775621726)] 76 | #[case::case_30_47("tests/samples/30-47.csv", -0.6103425929117927)] 77 | #[case::case_30_48("tests/samples/30-48.csv", -0.07525261340272364)] 78 | #[case::close_to_minus_0_13("tests/samples/minus_0_13.csv", -0.13423264098831872)] 79 | #[case::close_to_minus_0_99("tests/samples/minus_0_99.csv", -0.9989769231734277)] 80 | #[case::close_to_minus_0_99999("tests/samples/minus_0_99999.csv", -0.9999884228170087)] 81 | #[case::close_to_minus_0_993("tests/samples/minus_0_993.csv", -0.993785049929284)] 82 | #[case::zeros("tests/samples/zeros.csv", 0.175680730580782)] 83 | #[case::neg_1938("tests/samples/1938.csv", -0.5945650822679239)] 84 | fn test_xirr_samples(#[case] input: &str, #[case] expected: f64) { 85 | let result = Python::with_gil(|py| { 86 | let payments = PaymentsLoader::from_csv(py, input).to_records(); 87 | let rate: Option = pyxirr_call!(py, "xirr", (payments,)); 88 | 89 | if let Some(rate) = rate { 90 | let xnpv: f64 = pyxirr_call!(py, "xnpv", (rate, payments)); 91 | assert_almost_eq!(xnpv, 0.0, 1e-3); 92 | } 93 | 94 | rate.unwrap_or(f64::NAN) 95 | }); 96 | 97 | if result.is_nan() { 98 | assert!(expected.is_nan(), "assertion failed: expected {expected}, found NaN"); 99 | } else { 100 | assert_almost_eq!(result, expected); 101 | } 102 | } 103 | 104 | #[rstest] 105 | fn test_xirr_silent() { 106 | Python::with_gil(|py| { 107 | let args = (PyList::empty(py), PyList::empty(py)); 108 | let err = pyxirr_call_impl!(py, "xirr", args).unwrap_err(); 109 | assert!(err.is_instance(py, py.get_type::())); 110 | 111 | let result: Option = pyxirr_call!(py, "xirr", args, py_dict!(py, "silent" => true)); 112 | assert!(result.is_none()); 113 | }) 114 | } 115 | 116 | #[rstest] 117 | fn test_xfv() { 118 | // http://westclintech.com/SQL-Server-Financial-Functions/SQL-Server-XFV-function 119 | Python::with_gil(|py| { 120 | let args = ( 121 | PyDate::new(py, 2011, 2, 1).unwrap(), 122 | PyDate::new(py, 2011, 3, 1).unwrap(), 123 | PyDate::new(py, 2012, 2, 1).unwrap(), 124 | 0.00142, 125 | 0.00246, 126 | 100000., 127 | ); 128 | let result: f64 = pyxirr_call!(py, "xfv", args); 129 | assert_almost_eq!(result, 100235.088391894); 130 | }); 131 | } 132 | 133 | #[rstest] 134 | fn test_xnfv() { 135 | Python::with_gil(|py| { 136 | let payments = PaymentsLoader::from_csv(py, "tests/samples/xnfv.csv").to_records(); 137 | let result: f64 = pyxirr_call!(py, "xnfv", (0.0250, payments)); 138 | assert_almost_eq!(result, 57238.1249299303); 139 | }); 140 | } 141 | 142 | #[rstest] 143 | fn test_xnfv_silent() { 144 | Python::with_gil(|py| { 145 | let dates = vec!["2021-01-01", "2022-01-01"].into_py(py); 146 | let amounts = vec![1000, 100].into_py(py); 147 | let args = (0.0250, dates, amounts); 148 | let kwargs = py_dict!(py); 149 | let result: PyResult<_> = pyxirr_call_impl!(py, "xnfv", args.clone(), kwargs); 150 | 151 | assert!(result.is_err()); 152 | let kwargs = py_dict!(py, "silent" => true); 153 | let result: Option = pyxirr_call!(py, "xnfv", args, kwargs); 154 | assert!(result.is_none()); 155 | }); 156 | } 157 | 158 | #[rstest] 159 | fn test_sum_xfv_eq_xnfv() { 160 | Python::with_gil(|py| { 161 | let rate = 0.0250; 162 | let (dates, amounts) = PaymentsLoader::from_csv(py, "tests/samples/xnfv.csv").to_columns(); 163 | 164 | let xnfv_result: f64 = pyxirr_call!(py, "xnfv", (rate, dates, amounts)); 165 | 166 | let builtins = py.import("builtins").unwrap(); 167 | let locals = py_dict!(py, "dates" => dates); 168 | let min_date = py.eval("min(dates)", Some(locals), Some(builtins.dict())).unwrap(); 169 | let max_date = py.eval("max(dates)", Some(locals), Some(builtins.dict())).unwrap(); 170 | 171 | let sum_xfv_result: f64 = dates 172 | .iter() 173 | .unwrap() 174 | .map(Result::unwrap) 175 | .zip(amounts.iter().unwrap().map(Result::unwrap)) 176 | .map(|(date, amount)| -> f64 { 177 | pyxirr_call!(py, "xfv", (min_date, date, max_date, rate, rate, amount)) 178 | }) 179 | .sum(); 180 | 181 | assert_almost_eq!(xnfv_result, sum_xfv_result); 182 | }); 183 | } 184 | 185 | // https://www.mathworks.com/help/finance/xirr.html 186 | #[rstest] 187 | #[case("30/360 SIA", 0.100675477282743)] // 1 188 | #[case("act/360", 0.0991988898057063)] // 2 189 | #[case("act/365F", 0.10064378342638)] // 3 190 | #[case("30/360 ISDA", 0.100675477282743)] // 5 191 | #[case("30E/360", 0.100675477282743)] // 6 192 | #[case("act/act ISDA", 0.100739648987346)] // 12 193 | fn test_xirr_day_count(#[case] day_count: &str, #[case] expected: f64) { 194 | Python::with_gil(|py| { 195 | let dates = ["01/12/2007", "02/14/2008", "03/03/2008", "06/14/2008", "12/01/2008"]; 196 | let amounts = [-10000, 2500, 2000, 3000, 4000]; 197 | 198 | let kwargs = py_dict!(py, "day_count" => day_count); 199 | let value: f64 = pyxirr_call!(py, "xirr", (dates, amounts), kwargs); 200 | 201 | assert_almost_eq!(value, expected); 202 | }) 203 | } 204 | --------------------------------------------------------------------------------