Repository: Anexen/pyxirr (language: python)

Issue:
Add support for different day count conventions
Currently XIRR only works for debt instruments with ACT/ACT conventions. I suggest 30/360, ISMA-30/360 and ACT/360 also be added and be an additional parameter for XIRR function.  




Discussion / hints:
Hi @askmedov, thanks for the suggestion.
I have never used different day count conventions before but i figured them out and I can implement it. ETA is 2-3 weeks.

If I've got this right, XIRR is now working with ACT/365F convention, isn't it? (because of `(di−d0)/365` in [formula](https://anexen.github.io/pyxirr/functions.html#xirr))
My source is http://www.deltaquants.com/day-count-conventions


@Anexen yep, it's ACT/365F . Thanks a lot for the response! There are many hidden stones with the project, but it may be beneficial to start with the most popular conventions first - 30/360, ISMA-30/360 and ACT/360
@askmedov, I want to make the library more correct and useful. What other pitfalls do you see? All formulas that are used in PyXIRR are well [documented](https://anexen.github.io/pyxirr/functions.html). 
@Anexen i'm not entirely sure how to handle leap years (if necessary)

Fix patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 3c6a54e..12f3695 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -14,16 +14,9 @@ on:
 
 jobs:
   test:
-    runs-on: ${{ matrix.os }}
-    strategy:
-      matrix:
-        python-version: ['3.9']
-        os: ['ubuntu-latest']
+    runs-on: "ubuntu-latest"
     steps:
       - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
-        with:
-          python-version: ${{ matrix.python-version }}
       - name: Install Rust toolchain
         uses: actions-rs/toolchain@v1
         with:
@@ -79,37 +72,35 @@ jobs:
             target
           key: ${{ runner.os.platform }}-cargo-${{ hashFiles('**/Cargo.lock') }}
 
+      - run: rustup target list
+
       - name: Build Wheels - Linux
         if: matrix.os.platform == 'ubuntu-latest'
         uses: messense/maturin-action@v1
         with:
-          maturin-version: v0.13.7
+          maturin-version: v0.14.13
           manylinux: auto
           args: -i python${{ matrix.python.version }} --release --strip
 
-      - name: Build Wheels - MacOS [aarch64]
-        if: ${{ matrix.os.platform == 'macos-latest' && matrix.os.arch == 'aarch64' }}
-        run: rustup target add aarch64-apple-darwin
-
       - name: Build Wheels - MacOS [aarch64]
         if: ${{ matrix.os.platform == 'macos-latest' && matrix.os.arch == 'aarch64' }}
         uses: messense/maturin-action@v1
         with:
-          maturin-version: v0.13.7
+          maturin-version: v0.14.13
           args: -i python --release --target aarch64-apple-darwin --strip
 
       - name: Build Wheels - MacOS [x86_64]
         if: ${{ matrix.os.platform == 'macos-latest' && matrix.os.arch != 'aarch64' }}
         uses: messense/maturin-action@v1
         with:
-          maturin-version: v0.13.7
+          maturin-version: v0.14.13
           args: -i python --release --universal2 --strip
 
       - name: Build Wheels - Windows
         if: matrix.os.platform == 'windows-latest'
         uses: messense/maturin-action@v1
         with:
-          maturin-version: v0.13.7
+          maturin-version: v0.14.13
           args: -i python --release --strip
 
       - name: Upload wheels
@@ -143,7 +134,7 @@ jobs:
         env:
           PYO3_CROSS_LIB_DIR: /opt/python/${{ matrix.python.abi }}/lib
         with:
-          maturin-version: v0.13.7
+          maturin-version: v0.14.13
           target: ${{ matrix.target }}
           manylinux: auto
           args: -i python3.9 --release --strip
@@ -164,16 +155,15 @@ jobs:
           - i686-unknown-linux-musl
           - aarch64-unknown-linux-musl
           - armv7-unknown-linux-musleabihf
-        python_version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
     steps:
       - uses: actions/checkout@v3
       - name: Build Wheels - musl
         uses: messense/maturin-action@v1
         with:
-          maturin-version: v0.13.7
+          maturin-version: v0.14.13
           target: ${{ matrix.target }}
           manylinux: musllinux_1_2
-          args: --release --strip -i ${{ matrix.python_version }}
+          args: --release --strip -i 3.7 3.8 3.9 3.10 3.11
       - name: Upload wheels
         uses: actions/upload-artifact@v3
         with:
@@ -188,9 +178,6 @@ jobs:
       - uses: actions/download-artifact@v3
         with:
           name: wheels
-      - uses: actions/setup-python@v4
-        with:
-          python-version: 3.9
 
       - run: pip install maturin
 
@@ -201,7 +188,7 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: PyPi publish
+      - name: PyPI publish
         env:
           MATURIN_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
         run: find . -name 'pyxirr*' | xargs -l maturin upload --skip-existing --username __token__
diff --git a/Cargo.lock b/Cargo.lock
index 42bef82..db8592f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,17 +2,6 @@
 # It is not intended for manual editing.
 version = 3
 
-[[package]]
-name = "ahash"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
-dependencies = [
- "getrandom",
- "once_cell",
- "version_check",
-]
-
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -126,17 +115,6 @@ dependencies = [
  "slab",
 ]
 
-[[package]]
-name = "getrandom"
-version = "0.2.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi",
-]
-
 [[package]]
 name = "indoc"
 version = "1.0.6"
@@ -176,9 +154,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
 [[package]]
 name = "memoffset"
-version = "0.6.5"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
 dependencies = [
  "autocfg",
 ]
@@ -235,17 +213,17 @@ dependencies = [
 
 [[package]]
 name = "numpy"
-version = "0.17.2"
+version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a462c1af5ba1fddec1488c4646993a23ae7931f9e170ccba23e9c7c834277797"
+checksum = "96b0fee4571867d318651c24f4a570c3f18408cf95f16ccb576b3ce85496a46e"
 dependencies = [
- "ahash",
  "libc",
  "ndarray",
  "num-complex",
  "num-integer",
  "num-traits",
  "pyo3",
+ "rustc-hash",
 ]
 
 [[package]]
@@ -300,9 +278,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3"
-version = "0.17.3"
+version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "268be0c73583c183f2b14052337465768c07726936a260f480f0857cb95ba543"
+checksum = "06a3d8e8a46ab2738109347433cb7b96dffda2e4a218b03ef27090238886b147"
 dependencies = [
  "cfg-if",
  "indoc",
@@ -317,9 +295,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-build-config"
-version = "0.17.3"
+version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28fcd1e73f06ec85bf3280c48c67e731d8290ad3d730f8be9dc07946923005c8"
+checksum = "75439f995d07ddfad42b192dfcf3bc66a7ecfd8b4a1f5f6f046aa5c2c5d7677d"
 dependencies = [
  "once_cell",
  "target-lexicon",
@@ -327,9 +305,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-ffi"
-version = "0.17.3"
+version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f6cb136e222e49115b3c51c32792886defbfb0adead26a688142b346a0b9ffc"
+checksum = "839526a5c07a17ff44823679b68add4a58004de00512a95b6c1c98a6dcac0ee5"
 dependencies = [
  "libc",
  "pyo3-build-config",
@@ -337,9 +315,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-macros"
-version = "0.17.3"
+version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94144a1266e236b1c932682136dc35a9dee8d3589728f68130c7c3861ef96b28"
+checksum = "bd44cf207476c6a9760c4653559be4f206efafb924d3e4cbf2721475fc0d6cc5"
 dependencies = [
  "proc-macro2",
  "pyo3-macros-backend",
@@ -349,9 +327,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-macros-backend"
-version = "0.17.3"
+version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8df9be978a2d2f0cdebabb03206ed73b11314701a5bfe71b0d753b81997777f"
+checksum = "dc1f43d8e30460f36350d18631ccf85ded64c059829208fe680904c65bcd0a4c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -394,9 +372,9 @@ dependencies = [
 
 [[package]]
 name = "rstest"
-version = "0.15.0"
+version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9c9dc66cc29792b663ffb5269be669f1613664e69ad56441fdb895c2347b930"
+checksum = "b07f2d176c472198ec1e6551dc7da28f1c089652f66a7b722676c2238ebc0edf"
 dependencies = [
  "futures",
  "futures-timer",
@@ -406,17 +384,24 @@ dependencies = [
 
 [[package]]
 name = "rstest_macros"
-version = "0.14.0"
+version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5015e68a0685a95ade3eee617ff7101ab6a3fc689203101ca16ebc16f2b89c66"
+checksum = "7229b505ae0706e64f37ffc54a9c163e11022a6636d58fe1f3f52018257ff9f7"
 dependencies = [
  "cfg-if",
  "proc-macro2",
  "quote",
  "rustc_version",
  "syn",
+ "unicode-ident",
 ]
 
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
 [[package]]
 name = "rustc_version"
 version = "0.4.0"
@@ -489,9 +474,9 @@ checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.2"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
 
 [[package]]
 name = "unindent"
@@ -499,18 +484,6 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "52fee519a3e570f7df377a06a1a7775cdbfb7aa460be7e08de2b1f0e69973a44"
 
-[[package]]
-name = "version_check"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
-
-[[package]]
-name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
-
 [[package]]
 name = "windows-sys"
 version = "0.36.1"
diff --git a/Cargo.toml b/Cargo.toml
index 9f7b6c0..e9e640b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,14 @@ edition = "2021"
 description = "Rust-powered collection of financial functions for Python."
 readme = "README.md"
 homepage = "https://github.com/Anexen/pyxirr"
-keywords = ["python", "fast", "financial", "xirr", "cashflow"]
+keywords = [
+    "python",
+    "fast",
+    "financial",
+    "xirr",
+    "cashflow",
+    "day count convention"
+]
 license = "Unlicense"
 
 [lib]
@@ -14,12 +21,12 @@ name = "pyxirr"
 crate-type = ["rlib", "cdylib"]
 
 [dependencies]
-pyo3 = "0.17"
-numpy = "0.17"
+pyo3 = "0.18"
+numpy = "0.18"
 time = { version = "0.3", features = ["parsing", "macros"] }
 
 [dev-dependencies]
-rstest = "0.15"
+rstest = "0.16"
 
 [features]
 default = ["pyo3/extension-module"]
diff --git a/README.md b/README.md
index 1bb0648..136a102 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,10 @@ PyXIRR stands for "Python XIRR" (for historical reasons), but contains many othe
 Features:
 
 - correct
-- blazingly fast
+- supports different day count conventions (e.g. ACT/360, 30E/360, etc.)
 - works with different input data types (iterators, numpy arrays, pandas DataFrames)
 - no external dependencies
+- blazingly fast
 
 # Installation
 
@@ -57,7 +58,7 @@ xirr(dict(zip(dates, amounts)))
 xirr(['2020-01-01', '2021-01-01'], [-1000, 1200])
 ```
 
-Numpy and Pandas support
+### Numpy and Pandas
 
 ```python
 import numpy as np
@@ -84,7 +85,20 @@ df = pd.DataFrame(
 df.apply(xirr)  # Series(index=["one", "two"], data=[5.09623547168478, 8.780801977141174])
 ```
 
-Other financial functions:
+### Day count conventions
+
+Check out the available options on the [docs/day-count-conventions](https://anexen.github.io/pyxirr/functions.html#day-count-conventions).
+
+```python
+from pyxirr import DayCount
+
+xirr(dates, amounts, day_count=DayCount.ACT_360)
+
+# parse day count from string
+xirr(dates, amounts, day_count="30E/360")
+```
+
+### Other financial functions
 
 ```python
 import pyxirr
diff --git a/dev-requirements.txt b/dev-requirements.txt
index ea930ab..1417be9 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,4 +1,4 @@
-maturin==0.13.*
+maturin==0.14.*
 numpy==1.*
 pandas==1.*
 numpy-financial==1.*
diff --git a/docs/functions.md b/docs/functions.md
index a64f597..2eaa45d 100644
--- a/docs/functions.md
+++ b/docs/functions.md
@@ -24,6 +24,54 @@ CashFlowDict = Dict[DateLike, Amount]
 CashFlow = Union[CashFlowSeries, CashFlowTable, CashFlowDict]
 ```
 
+## Day Count Conventions
+
+The [day count convention](https://en.wikipedia.org/wiki/Day_count_convention)
+determines how interest accrues over time in a variety of transactions,
+including bonds, swaps, bills and loans.
+
+The following conventions are supported:
+
+| Name               | Constant                   | Also known                      |
+| ------------------ | -------------------------- | ------------------------------- |
+| Actual/Actual ISDA | DayCount.ACT_ACT_ISDA      | Act/Act ISDA                    |
+| Actual/365 Fixed   | DayCount.ACT_365F          | Act/365F, English               |
+| Actual/365.25      | DayCount.ACT_365_25        |                                 |
+| Actual/364         | DayCount.ACT_364           |                                 |
+| Actual/360         | DayCount.ACT_360           | French                          |
+| 30/360 ISDA        | DayCount.THIRTY_360_ISDA   | Bond basis                      |
+| 30E/360            | DayCount.THIRTY_E_360      | 30/360 ISMA, Eurobond basis     |
+| 30E+/360           | DayCount.THIRTY_E_PLUS_360 |                                 |
+| 30E/360 ISDA       | DayCount.THIRTY_E_360_ISDA | 30E/360 German, German          |
+| 30U/360            | DayCount.THIRTY_U_360      | 30/360 US, 30US/360, 30/360 SIA |
+| NL/365             | DayCount.NL_365            | Actual/365 No leap year         |
+| NL/360             | DayCount.NL_360            |                                 |
+
+Definition:
+
+```python
+def year_fraction(
+    d1: DateLike,
+    d2: DateLike,
+    day_count: DayCount,
+) -> float:
+    ...
+
+```
+
+Usage:
+
+```python
+from pyxirr import year_fraction, DayCount
+year_fraction("2019-11-09", "2020-03-05", DayCount.THIRTY_E_360)
+year_fraction("2019-11-09", "2020-03-05", "act/360")
+```
+
+See also:
+
+- [2006 ISDA definitions](https://www.rbccm.com/assets/rbccm/docs/legal/doddfrank/Documents/ISDALibrary/2006%20ISDA%20Definitions.pdf)
+- http://www.deltaquants.com/day-count-conventions
+
 ## Exceptions
 
 - `InvalidPaymentsError`. Occurs if either:
@@ -114,11 +162,13 @@ def xfv(
     cash_flow_rate: Rate,  # annual rate
     end_rate: Rate,  # annual rate
     cash_flow: Amount,
+    *,
+    day_count: DayCount = DayCount.ACT_365F,
 ) -> Optional[float]:
     ...
 ```
 
-When:
+Where:
 
 - `start_date`: the starting date for the annual interest rates used in the XFV calculation.
 - `cash_flow_date`: the date on which the cash flows occurs.
@@ -126,6 +176,7 @@ When:
 - `cash_flow_rate`: the annual interest rate for the cash flow date. This should be the interest rate from the `start_date` to the `cash_flow_date`.
 - `end_rate`: the annual interest rate for the end date. This should be the interest rate from the `start_date` to the `end_date`.
 - `cash_flow`: the cash flow value.
+- `day_count`: Day count convention.
 
 See also: [XLeratorDB.XFV](http://westclintech.com/SQL-Server-Financial-Functions/SQL-Server-XFV-function)
 
@@ -164,6 +215,7 @@ def xnfv(
     amounts: Optional[AmountArray] = None,
     *,
     silent: bool = False
+    day_count: DayCount = DayCount.ACT_365F,
 ) -> Optional[float]:
     ...
 ```
@@ -384,6 +436,7 @@ def xnpv(
     amounts: Optional[AmountArray] = None,
     *,
     silent: bool = False
+    day_count: DayCount = DayCount.ACT_365F,
 ) -> Optional[float]:
     ...
 ```
@@ -392,7 +445,7 @@ XNPV is calculated as follows:
 
 $$XNPV=\sum_{i=1}^n \frac{P_i}{(1 + rate)^{(d_i - d_0)/365}}$$
 
-where:
+Where:
 
 - `di` = the ith, or last, payment date.
 - `d0` = the 0th payment date.
@@ -547,6 +600,7 @@ def xirr(
     *,
     guess: Guess = 0.1,
     silent: bool = False,
+    day_count: DayCount = DayCount.ACT_365F,
 ) -> Optional[float]:
     ...
 ```
diff --git a/pyproject.toml b/pyproject.toml
index 7f3c0bf..2c03d3a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,7 +16,7 @@ classifiers = [
 ]
 
 [build-system]
-requires = ["maturin>=0.13,<0.14"]
+requires = ["maturin>=0.14,<0.15"]
 build-backend = "maturin"
 
 [tool.maturin]
diff --git a/pyxirr.pyi b/pyxirr.pyi
index 76ee729..80760d0 100644
--- a/pyxirr.pyi
+++ b/pyxirr.pyi
@@ -10,13 +10,12 @@ from typing import (
     Union,
     overload,
     Hashable,
-    Literal,
 )
 
 if sys.version_info >= (3, 8):
-    from typing import Protocol
+    from typing import Protocol, Literal
 else:
-    from typing_extensions import Protocol
+    from typing_extensions import Protocol, Literal
 
 # We are using protocols because mypy does not support dynamic type hints for
 # optional libraries, e.g in the ideal world:
@@ -104,11 +103,31 @@ class _Timestamp(Protocol):
 
 # fmt: on
 
+
+class DayCount:
+    ACT_ACT_ISDA: "DayCount"
+    ACT_365F: "DayCount"
+    ACT_365_25: "DayCount"
+    ACT_364: "DayCount"
+    ACT_360: "DayCount"
+    THIRTY_360_ISDA: "DayCount"
+    THIRTY_E_360: "DayCount"
+    THIRTY_E_PLUS_360: "DayCount"
+    THIRTY_E_360_ISDA: "DayCount"
+    THIRTY_U_360: "DayCount"
+    NL_365: "DayCount"
+    NL_360: "DayCount"
+
+    def __init__(self, day_count: str):
+        ...
+
+
 # rate as decimal, not percentage, normally between [-1, 1]
 _Rate = Union[float, Decimal]
 _Period = Union[int, float, Decimal]
 _Guess = Optional[_Rate]
 _Amount = Union[int, float, Decimal]
+_DayCount = Union[DayCount | str]
 
 _DateLike = Union[str, date, datetime, _datetime64, _Timestamp]
 _Payment = Tuple[_DateLike, _Amount]
@@ -154,6 +173,8 @@ def xfv(
 def xnfv(
     rate: _Rate,  # annual rate
     dates: _CashFlow,
+    *,
+    day_count: _DayCount = DayCount.ACT_365F,
 ) -> Optional[float]:
     ...
 
@@ -163,6 +184,8 @@ def xnfv(
     rate: _Rate,  # annual rate
     dates: _DateLikeArray,
     amounts: _AmountArray,
+    *,
+    day_count: _DayCount = DayCount.ACT_365F,
 ) -> Optional[float]:
     ...
 
@@ -193,6 +216,7 @@ def xnpv(
     dates: _CashFlow,
     *,
     silent: bool = False,
+    day_count: _DayCount = DayCount.ACT_365F,
 ) -> Optional[float]:
     ...
 
@@ -204,6 +228,7 @@ def xnpv(
     amounts: _AmountArray,
     *,
     silent: bool = False,
+    day_count: _DayCount = DayCount.ACT_365F,
 ) -> Optional[float]:
     ...
 
@@ -292,6 +317,7 @@ def xirr(
     *,
     guess: _Guess = 0.1,
     silent: bool = False,
+    day_count: _DayCount = DayCount.ACT_365F,
 ) -> Optional[float]:
     ...
 
@@ -302,5 +328,6 @@ def xirr(
     *,
     guess: _Guess = 0.1,
     silent: bool = False,
+    day_count: _DayCount = DayCount.ACT_365F,
 ) -> Optional[float]:
     ...
diff --git a/src/conversions.rs b/src/conversions.rs
index 6b6a08f..8afdb25 100644
--- a/src/conversions.rs
+++ b/src/conversions.rs
@@ -1,9 +1,66 @@
-use crate::core::DateLike;
+use std::str::FromStr;
+
+use crate::core::{DateLike, DayCount};
+use numpy::datetime::{units, Datetime as datetime64};
 use numpy::PyArray1;
-use pyo3::{exceptions, prelude::*, types::*};
+use pyo3::{
+    exceptions::{PyTypeError, PyValueError},
+    prelude::*,
+    types::*,
+};
 use time::Date;
 
-const SECONDS_IN_DAY: i64 = 24 * 60 * 60;
+// time::Date::from_ordinal_date(1970, 1).unwrap().to_julian_day();
+static UNIX_EPOCH_JULIAN_DAY: i32 = 2440588;
+
+#[derive(FromPyObject)]
+pub enum PyDayCount {
+    String(String),
+    DayCount(DayCount),
+}
+
+impl TryInto<DayCount> for PyDayCount {
+    type Error = PyErr;
+
+    fn try_into(self) -> Result<DayCount, Self::Error> {
+        match self {
+            PyDayCount::String(s) => DayCount::of(&s),
+            PyDayCount::DayCount(d) => Ok(d),
+        }
+    }
+}
+
+#[pymethods]
+impl DayCount {
+    #[staticmethod]
+    fn of(value: &str) -> PyResult<Self> {
+        DayCount::from_str(value).map_err(|e| PyValueError::new_err(e))
+    }
+
+    fn __str__(&self) -> String {
+        self.to_string()
+    }
+}
+
+struct DaysSinceUnixEpoch(i32);
+
+impl<'s> FromPyObject<'s> for DaysSinceUnixEpoch {
+    fn extract(obj: &'s PyAny) -> PyResult<Self> {
+        obj.extract::<i64>().map(|x| Self(x as i32))
+    }
+}
+
+impl From<DaysSinceUnixEpoch> for DateLike {
+    fn from(value: DaysSinceUnixEpoch) -> Self {
+        Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + value.0).unwrap().into()
+    }
+}
+
+impl From<i64> for DateLike {
+    fn from(value: i64) -> Self {
+        Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + (value as i32)).unwrap().into()
+    }
+}
 
 impl From<&PyDate> for DateLike {
     fn from(value: &PyDate) -> Self {
@@ -17,6 +74,15 @@ impl From<&PyDate> for DateLike {
     }
 }
 
+impl From<&datetime64<units::Days>> for DateLike {
+    fn from(value: &datetime64<units::Days>) -> Self {
+        let days_since_unix_epoch: i32 = Into::<i64>::into(*value) as i32;
+        let date = Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + days_since_unix_epoch).unwrap();
+
+        date.into()
+    }
+}
+
 impl<'s> FromPyObject<'s> for DateLike {
     fn extract(obj: &'s PyAny) -> PyResult<Self> {
         if let Ok(py_date) = obj.downcast::<PyDate>() {
@@ -27,21 +93,19 @@ impl<'s> FromPyObject<'s> for DateLike {
             return py_string
                 .to_str()?
                 .parse::<DateLike>()
-                .map_err(|e| exceptions::PyValueError::new_err(e.to_string()));
+                .map_err(|e| PyValueError::new_err(e.to_string()));
         }
 
         match obj.get_type().name()? {
-            "datetime64" => {
-                Ok(obj.call_method1("astype", ("datetime64[D]",))?.extract::<i32>()?.into())
-            }
-            "Timestamp" => {
-                let timestamp: i64 =
-                    obj.call_method0("to_pydatetime")?.call_method0("timestamp")?.extract()?;
+            "datetime64" => Ok(obj
+                .call_method1("astype", ("datetime64[D]",))?
+                .call_method1("astype", ("int",))?
+                .extract::<DaysSinceUnixEpoch>()?
+                .into()),
 
-                Ok(((timestamp / SECONDS_IN_DAY) as i32).into())
-            }
+            "Timestamp" => Ok(obj.call_method0("to_pydatetime")?.downcast::<PyDate>()?.into()),
 
-            other => Err(exceptions::PyTypeError::new_err(format!(
+            other => Err(PyTypeError::new_err(format!(
                 "Type {:?} is not understood. Expected: date",
                 other
             ))),
@@ -59,12 +123,11 @@ where
 fn extract_date_series_from_numpy(series: &PyAny) -> PyResult<Vec<DateLike>> {
     Ok(series
         .call_method1("astype", ("datetime64[D]",))?
-        .call_method1("astype", ("int32",))?
-        .extract::<&PyArray1<i32>>()?
+        .downcast::<PyArray1<datetime64<units::Days>>>()?
         .readonly()
         .as_slice()?
         .iter()
-        .map(|&x| x.into())
+        .map(|x| x.into())
         .collect())
 }
 
@@ -162,3 +225,48 @@ pub fn extract_payments(
         _ => extract_records(dates),
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use pyo3::{prelude::*, types::PyDict};
+    use rstest::rstest;
+    use time::{Date, Month};
+
+    use crate::core::DateLike;
+
+    fn get_locals<'p>(py: &'p Python) -> &'p PyDict {
+        py.eval("{ 'np': __import__('numpy') }", None, None).unwrap().downcast::<PyDict>().unwrap()
+    }
+
+    #[rstest]
+    #[cfg_attr(feature = "nonumpy", ignore)]
+    fn test_extract_from_numpy_datetime_array() {
+        Python::with_gil(|py| {
+            let locals = get_locals(&py);
+            let data = py
+                .eval(
+                    "np.array(['2007-02-01', '2009-09-30'], dtype='datetime64[D]')",
+                    Some(locals),
+                    None,
+                )
+                .unwrap();
+            let dt: Vec<DateLike> = data.extract().unwrap();
+            let exp: DateLike = Date::from_calendar_date(2007, Month::February, 1).unwrap().into();
+
+            assert_eq!(dt[0], exp);
+        })
+    }
+
+    #[rstest]
+    #[cfg_attr(feature = "nonumpy", ignore)]
+    fn test_extract_from_numpy_datetime() {
+        Python::with_gil(|py| {
+            let locals = get_locals(&py);
+            let data = py.eval("np.datetime64('2007-02-01', '[D]')", Some(locals), None).unwrap();
+            let dt: DateLike = data.extract().unwrap();
+            let exp: DateLike = Date::from_calendar_date(2007, Month::February, 1).unwrap().into();
+
+            assert_eq!(dt, exp);
+        })
+    }
+}
diff --git a/src/core/mod.rs b/src/core/mod.rs
index bedb038..af35b59 100644
--- a/src/core/mod.rs
+++ b/src/core/mod.rs
@@ -7,4 +7,4 @@ mod scheduled;
 
 pub use models::{DateLike, InvalidPaymentsError};
 pub use periodic::{fv, ipmt, irr, mirr, nfv, nper, npv, pmt, ppmt, pv, rate};
-pub use scheduled::{xfv, xirr, xnfv, xnpv};
+pub use scheduled::{days_between, xfv, xirr, xnfv, xnpv, year_fraction, DayCount};
diff --git a/src/core/models.rs b/src/core/models.rs
index fcd73fa..d163567 100644
--- a/src/core/models.rs
+++ b/src/core/models.rs
@@ -1,52 +1,29 @@
 use std::error::Error;
-use std::fmt;
+use std::{fmt, str::FromStr};
 use time::{macros::format_description, Date};
 
 #[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Copy)]
-pub struct DateLike(i32);
+pub struct DateLike(Date);
 
-impl From<i32> for DateLike {
-    fn from(value: i32) -> Self {
-        Self(value)
+impl Into<Date> for DateLike {
+    fn into(self) -> Date {
+        self.0
     }
 }
 
 impl From<Date> for DateLike {
     fn from(value: Date) -> Self {
-        // See chrono.num_days_from_ce implementation.
-        // we know this wouldn't overflow since year is limited to 1/2^13 of i32's full range.
-        let mut year = value.year() - 1;
-        let mut ndays = 0;
-        if year < 0 {
-            let excess = 1 + (-year) / 400;
-            year += excess * 400;
-            ndays -= excess * 146_097;
-        }
-        let div_100 = year / 100;
-        ndays += ((year * 1461) >> 2) - div_100 + (div_100 >> 2);
-        ndays += value.ordinal() as i32;
-
-        Self(ndays)
-    }
-}
-
-impl std::ops::Sub for DateLike {
-    type Output = i32;
-
-    fn sub(self, other: DateLike) -> Self::Output {
-        self.0 - other.0
+        Self(value)
     }
 }
 
-impl std::ops::Sub for &DateLike {
-    type Output = i32;
-
-    fn sub(self, other: &DateLike) -> Self::Output {
-        self.0 - other.0
+impl AsRef<Date> for DateLike {
+    fn as_ref(&self) -> &Date {
+        &self.0
     }
 }
 
-impl std::str::FromStr for DateLike {
+impl FromStr for DateLike {
     type Err = time::error::Parse;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
diff --git a/src/core/scheduled/day_count.rs b/src/core/scheduled/day_count.rs
new file mode 100644
index 0000000..81d9036
--- /dev/null
+++ b/src/core/scheduled/day_count.rs
@@ -0,0 +1,340 @@
+use std::{cmp::min, fmt, str::FromStr};
+use time::{
+    util::{days_in_year_month, is_leap_year},
+    Date, Month,
+};
+
+#[pyo3::pyclass]
+#[pyo3(frozen)]
+#[derive(Debug, Clone, Copy)]
+#[allow(non_camel_case_types)]
+pub enum DayCount {
+    ACT_ACT_ISDA,
+    ACT_365F,
+    ACT_365_25,
+    ACT_364,
+    ACT_360,
+    THIRTY_360_ISDA,
+    THIRTY_E_360,
+    THIRTY_E_PLUS_360,
+    THIRTY_E_360_ISDA,
+    THIRTY_U_360,
+    NL_365,
+    NL_360,
+}
+
+impl Default for DayCount {
+    fn default() -> Self {
+        DayCount::ACT_365F
+    }
+}
+
+impl fmt::Display for DayCount {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        use DayCount::*;
+        let repr = match self {
+            ACT_ACT_ISDA => "Actual/Actual ISDA",
+            ACT_365F => "Actual/365F",
+            ACT_365_25 => "Actual/365.25",
+            ACT_364 => "Actual/364",
+            ACT_360 => "Actual/360",
+            THIRTY_360_ISDA => "30/360 ISDA",
+            THIRTY_E_360 => "30E/360",
+            THIRTY_E_PLUS_360 => "30E+/360",
+            THIRTY_E_360_ISDA => "30E/360 ISDA",
+            THIRTY_U_360 => "30U/360",
+            NL_365 => "NL/365",
+            NL_360 => "NL/360",
+        };
+        write!(f, "{}", repr)
+    }
+}
+
+impl FromStr for DayCount {
+    type Err = &'static str;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s.to_ascii_lowercase().as_str() {
+            "actual/actual" | "actual/actual isda" | "act/act" | "act/act isda" => {
+                Ok(DayCount::ACT_ACT_ISDA)
+            }
+            #[rustfmt::skip]
+            "actual/365 fixed" | "act/365 fixed" | "a/365 fixed"
+                | "actual/365f" | "act/365f" | "a/365f"
+                | "english" => Ok(DayCount::ACT_365F),
+            "actual/365.25" | "act/365.25" | "a/365.25" => Ok(DayCount::ACT_365_25),
+            "actual/364" | "act/364" | "a/364" => Ok(DayCount::ACT_364),
+            "actual/360" | "act/360" | "a/360" | "french" => Ok(DayCount::ACT_360),
+            "30/360" | "30/360 isda" | "bond basis" => Ok(DayCount::THIRTY_360_ISDA),
+            "30e/360" | "30/360 isma" | "eurobond basis" => Ok(DayCount::THIRTY_E_360),
+            "30u/360" | "30us/360" | "30/360 us" | "30/360 sia" => Ok(DayCount::THIRTY_U_360),
+            "30e+/360" => Ok(DayCount::THIRTY_E_PLUS_360),
+            "30e/360 isda" | "30e/360 german" | "german" => Ok(DayCount::THIRTY_E_360_ISDA),
+            "nl/365" | "nl365" => Ok(DayCount::NL_365),
+            "nl/360" | "nl360" => Ok(DayCount::NL_360),
+            _ => Err("Invalid Day Count Convention"),
+        }
+    }
+}
+
+pub fn year_fraction<T: AsRef<Date>>(d1: T, d2: T, day_count: DayCount) -> f64 {
+    let (d1, d2) = (d1.as_ref(), d2.as_ref());
+
+    use DayCount::*;
+    match day_count {
+        ACT_ACT_ISDA => {
+            let (normal_days, leap_days) = normal_leap_days(d1, d2);
+            normal_days as f64 / 365.0 + leap_days as f64 / 366.0
+        }
+        ACT_365F => days_between_act(d1, d2) as f64 / 365.0,
+        ACT_365_25 => days_between_act(d1, d2) as f64 / 365.25,
+        ACT_364 => days_between_act(d1, d2) as f64 / 364.0,
+        ACT_360 => days_between_act(d1, d2) as f64 / 360.0,
+        THIRTY_360_ISDA => days_between_30_360_isda(d1, d2) as f64 / 360.0,
+        THIRTY_E_360 => days_between_30_e_360(d1, d2) as f64 / 360.0,
+        THIRTY_E_PLUS_360 => days_between_30_e_plus_360(d1, d2) as f64 / 360.0,
+        THIRTY_E_360_ISDA => days_between_30_e_360_isda(d1, d2) as f64 / 360.0,
+        THIRTY_U_360 => days_between_30_u_360(d1, d2) as f64 / 360.0,
+        NL_365 => days_between_excluding_leap_days(d1, d2) as f64 / 365.0,
+        NL_360 => days_between_excluding_leap_days(d1, d2) as f64 / 360.0,
+    }
+}
+
+pub fn days_between<T: AsRef<Date>>(d1: T, d2: T, day_count: DayCount) -> i32 {
+    let (d1, d2) = (d1.as_ref(), d2.as_ref());
+
+    use DayCount::*;
+    match day_count {
+        ACT_ACT_ISDA | ACT_365F | ACT_365_25 | ACT_364 | ACT_360 => days_between_act(d1, d2),
+        THIRTY_360_ISDA => days_between_30_360_isda(d1, d2),
+        THIRTY_E_360 => days_between_30_e_360(d1, d2),
+        THIRTY_E_PLUS_360 => days_between_30_e_plus_360(d1, d2),
+        THIRTY_E_360_ISDA => days_between_30_e_360_isda(d1, d2),
+        THIRTY_U_360 => days_between_30_u_360(d1, d2),
+        NL_365 => days_between_excluding_leap_days(d1, d2),
+        NL_360 => days_between_excluding_leap_days(d1, d2),
+    }
+}
+
+fn days_between_act(d1: &Date, d2: &Date) -> i32 {
+    d2.to_julian_day() - d1.to_julian_day()
+}
+
+fn normal_leap_years(d1: &Date, d2: &Date) -> (i32, i32) {
+    let (y1, y2) = (d1.year(), d2.year());
+    let leap_years = (y1..=y2).filter(|&y| is_leap_year(y)).count() as i32;
+    let normal_years = y2 - y1 - leap_years + 1;
+    (normal_years, leap_years)
+}
+
+fn normal_leap_days(d1: &Date, d2: &Date) -> (i32, i32) {
+    let (mut normal, mut leap) = normal_leap_years(d1, d2);
+
+    normal *= 365;
+    leap *= 366;
+
+    if is_leap_year(d1.year()) {
+        leap -= d1.ordinal() as i32 - 1
+    } else {
+        normal -= d1.ordinal() as i32 - 1
+    }
+
+    if is_leap_year(d2.year()) {
+        leap -= 366 - d2.ordinal() as i32 + 1
+    } else {
+        normal -= 365 - d2.ordinal() as i32 + 1
+    }
+
+    (normal, leap)
+}
+
+fn days_between_excluding_leap_days(d1: &Date, d2: &Date) -> i32 {
+    days_between_act(d1, d2) - leap_days_between(d1, d2)
+}
+
+fn leap_days_between(d1: &Date, d2: &Date) -> i32 {
+    let (_, mut leap_days) = normal_leap_years(d1, d2);
+
+    if is_leap_year(d1.year()) && (d1.month() as u8) > 2 {
+        leap_days -= 1
+    }
+
+    if is_leap_year(d2.year()) && d2.ordinal() < 60 {
+        leap_days -= 1
+    }
+
+    leap_days
+}
+
+fn days_between_30_360_isda(d1: &Date, d2: &Date) -> i32 {
+    let d1_day = min(d1.day(), 30);
+    let d2_day = if d1_day >= 30 { min(d2.day(), 30) } else { d2.day() };
+    days_between_30_360(d1, d2, d1_day, d2_day)
+}
+
+fn days_between_30_e_360(d1: &Date, d2: &Date) -> i32 {
+    let d1_day = min(d1.day(), 30);
+    let d2_day = min(d2.day(), 30);
+    days_between_30_360(d1, d2, d1_day, d2_day)
+}
+
+fn days_between_30_e_360_isda(d1: &Date, d2: &Date) -> i32 {
+    let d1_day = if is_last_day_of_feb(d1) { 30 } else { min(d1.day(), 30) };
+    let d2_day = if is_last_day_of_feb(d2) { 30 } else { min(d2.day(), 30) };
+    days_between_30_360(d1, d2, d1_day, d2_day)
+}
+
+fn days_between_30_e_plus_360(d1: &Date, d2: &Date) -> i32 {
+    let d1_day = min(d1.day(), 30);
+    if d2.day() == 31 {
+        let d2 = d2.next_day().unwrap();
+        days_between_30_360(d1, &d2, d1_day, d2.day())
+    } else {
+        days_between_30_360(d1, d2, d1_day, d2.day())
+    }
+}
+
+fn days_between_30_u_360(d1: &Date, d2: &Date) -> i32 {
+    let d1_day = if is_last_day_of_feb(d1) { 30 } else { min(d1.day(), 30) };
+    let d2_day = if is_last_day_of_feb(d1) && is_last_day_of_feb(d2) {
+        30
+    } else if d1_day >= 30 {
+        min(d2.day(), 30)
+    } else {
+        d2.day()
+    };
+
+    days_between_30_360(d1, d2, d1_day, d2_day)
+}
+
+fn days_between_30_360(d1: &Date, d2: &Date, d1_day: u8, d2_day: u8) -> i32 {
+    360 * (d2.year() - d1.year())
+        + 30 * (d2.month() as i32 - d1.month() as i32)
+        + (d2_day as i32 - d1_day as i32)
+}
+
+pub fn is_last_day_of_month(date: &Date) -> bool {
+    date.day() == days_in_year_month(date.year(), date.month())
+}
+
+pub fn is_last_day_of_feb(date: &Date) -> bool {
+    date.month() == Month::February && is_last_day_of_month(date)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::core::DateLike;
+    use rstest::rstest;
+
+    // test cases from http://www.deltaquants.com/day-count-conventions
+    #[rstest]
+    #[case("Act/Act", 4./365. + 58./366.)]
+    #[case("Act/365F", 62./365.)]
+    #[case("Act/360", 62./360.)]
+    // #[case("Act/365A", 62./365.)]
+    // #[case("Act/365L", 62./366.)]
+    #[case("NL/365", 62./365.)]
+    #[case("30/360 ISDA", 60./360.)]
+    #[case("30E/360", 60./360.)]
+    #[case("30E+/360", 60./360.)]
+    #[case("30E/360 German", 60./360.0)]
+    #[case("30/360 US", 60./360.)]
+    fn test_example_1(#[case] day_count: DayCount, #[case] expected: f64) {
+        let d1 = "2007-12-28".parse::<DateLike>().unwrap();
+        let d2 = "2008-02-28".parse::<DateLike>().unwrap();
+        assert_eq!(year_fraction(&d1, &d2, day_count), expected);
+    }
+
+    #[rstest]
+    #[case("Act/Act", 4./365. + 59./366.)]
+    #[case("Act/365F", 63./365.)]
+    #[case("Act/360", 63./360.)]
+    // #[case("Act/365A", 63./366.)]
+    // #[case("Act/365L", 63./366.)]
+    #[case("NL/365", 62./365.)]
+    #[case("30/360 ISDA", 61./360.)]
+    #[case("30E/360", 61./360.)]
+    #[case("30E+/360", 61./360.)]
+    #[case("30E/360 German", 62./360.)]
+    #[case("30/360 US", 61./360.)]
+    fn test_example_2(#[case] day_count: DayCount, #[case] expected: f64) {
+        let d1 = "2007-12-28".parse::<DateLike>().unwrap();
+        let d2 = "2008-02-29".parse::<DateLike>().unwrap();
+        assert_eq!(year_fraction(&d1, &d2, day_count), expected);
+    }
+
+    #[rstest]
+    #[case("Act/Act", 62./365. + 334./366.)]
+    #[case("Act/365F", 396./365.)]
+    #[case("Act/360", 396./360.)]
+    // #[case("Act/365A", 396./366.)]
+    // #[case("Act/365L", 396./366.)]
+    #[case("NL/365", 395./365.)]
+    #[case("30/360 ISDA", 390./360.)]
+    #[case("30E/360", 390./360.)]
+    #[case("30E+/360", 390./360.)]
+    #[case("30E/360 German", 390./360.)]
+    #[case("30/360 US", 390./360.)]
+    fn test_example_3(#[case] day_count: DayCount, #[case] expected: f64) {
+        let d1 = "2007-10-31".parse::<DateLike>().unwrap();
+        let d2 = "2008-11-30".parse::<DateLike>().unwrap();
+        assert_eq!(year_fraction(&d1, &d2, day_count), expected);
+    }
+
+    #[rstest]
+    #[case("Act/Act", 335./366. + 150./365.)]
+    #[case("Act/365F", 485./365.)]
+    #[case("Act/360", 485./360.)]
+    // #[case("Act/365A", 485./366.)]
+    // #[case("Act/365L", 485./365.)]
+    #[case("NL/365", 484./365.)]
+    #[case("30/360 ISDA", 480./360.)]
+    #[case("30E/360", 479./360.)]
+    #[case("30E+/360", 480./360.)]
+    #[case("30E/360 German", 479./360.)]
+    #[case("30/360 US", 480./360.)]
+    fn test_example_4(#[case] day_count: DayCount, #[case] expected: f64) {
+        let d1 = "2008-02-01".parse::<DateLike>().unwrap();
+        let d2 = "2009-05-31".parse::<DateLike>().unwrap();
+        assert_eq!(year_fraction(&d1, &d2, day_count), expected);
+    }
+
+    // https://westclintech.com/SQL-Server-Financial-Functions/SQL-Server-DAYS360-function
+    // Start Date, End Date, 30/360 US, 30E/360, 30E360 ISDA, Actual
+    static XLERATOR_DB_DATA: [(&'static str, &'static str, i32, i32, i32, i32); 22] = [
+        ("2007-01-15", "2007-01-30", 15, 15, 15, 15),
+        ("2007-01-15", "2007-02-15", 30, 30, 30, 31),
+        ("2007-01-15", "2007-07-15", 180, 180, 180, 181),
+        ("2007-09-30", "2008-03-31", 180, 180, 180, 183),
+        ("2007-09-30", "2007-10-31", 30, 30, 30, 31),
+        ("2007-09-30", "2008-09-30", 360, 360, 360, 366),
+        ("2007-01-15", "2007-01-31", 16, 15, 15, 16),
+        ("2007-01-31", "2007-02-28", 28, 28, 30, 28),
+        ("2007-02-28", "2007-03-31", 30, 32, 30, 31),
+        ("2006-08-31", "2007-02-28", 178, 178, 180, 181),
+        ("2007-02-28", "2007-08-31", 180, 182, 180, 184),
+        ("2007-02-14", "2007-02-28", 14, 14, 16, 14),
+        ("2007-02-26", "2008-02-29", 363, 363, 364, 368),
+        ("2008-02-29", "2009-02-28", 360, 359, 360, 365),
+        ("2008-02-29", "2008-03-30", 30, 31, 30, 30),
+        ("2008-02-29", "2008-03-31", 30, 31, 30, 31),
+        ("2007-02-28", "2007-03-05", 5, 7, 5, 5),
+        ("2007-10-31", "2007-11-28", 28, 28, 28, 28),
+        ("2007-08-31", "2008-02-29", 179, 179, 180, 182),
+        ("2008-02-29", "2008-08-31", 180, 181, 180, 184),
+        ("2008-08-31", "2009-02-28", 178, 178, 180, 181),
+        ("2009-02-28", "2009-08-31", 180, 182, 180, 184),
+    ];
+
+    #[rstest]
+    fn test_from_xleratordb() {
+        for row in XLERATOR_DB_DATA {
+            let ref d1 = row.0.parse::<DateLike>().unwrap();
+            let ref d2 = row.1.parse::<DateLike>().unwrap();
+            assert_eq!(days_between(d1, d2, DayCount::THIRTY_U_360), row.2);
+            assert_eq!(days_between(d1, d2, DayCount::THIRTY_E_360), row.3);
+            assert_eq!(days_between(d1, d2, DayCount::ACT_365F), row.5);
+        }
+    }
+}
diff --git a/src/core/scheduled/mod.rs b/src/core/scheduled/mod.rs
index 85d8abd..9695b64 100644
--- a/src/core/scheduled/mod.rs
+++ b/src/core/scheduled/mod.rs
@@ -1,5 +1,7 @@
+mod day_count;
 mod xirr;
 mod xnfv;
 
+pub use day_count::{days_between, year_fraction, DayCount};
 pub use xirr::{xirr, xnpv};
 pub use xnfv::{xfv, xnfv};
diff --git a/src/core/scheduled/xirr.rs b/src/core/scheduled/xirr.rs
index cbaed53..580380f 100644
--- a/src/core/scheduled/xirr.rs
+++ b/src/core/scheduled/xirr.rs
@@ -1,3 +1,4 @@
+use super::{year_fraction, DayCount};
 use crate::core::models::{validate, DateLike, InvalidPaymentsError};
 use crate::core::optimize::find_root;
 
@@ -5,46 +6,52 @@ pub fn xirr(
     dates: &[DateLike],
     amounts: &[f64],
     guess: Option<f64>,
+    day_count: Option<DayCount>,
 ) -> Result<f64, InvalidPaymentsError> {
     validate(amounts, Some(dates))?;
 
-    let ref deltas = precalculate_deltas(&dates);
+    let ref deltas = day_count_factor(&dates, day_count);
 
     Ok(find_root(
         guess.unwrap_or(0.1),
         &[(-0.99, 1.0, 0.01)],
-        |rate| xirr_result(amounts, deltas, rate),
-        |rate| xirr_result_deriv(amounts, deltas, rate),
+        |rate| xnpv_result(amounts, deltas, rate),
+        |rate| xnpv_result_deriv(amounts, deltas, rate),
     ))
 }
 
 /// Calculate the net present value of a series of payments at irregular intervals.
-pub fn xnpv(rate: f64, dates: &[DateLike], amounts: &[f64]) -> Result<f64, InvalidPaymentsError> {
+pub fn xnpv(
+    rate: f64,
+    dates: &[DateLike],
+    amounts: &[f64],
+    day_count: Option<DayCount>,
+) -> Result<f64, InvalidPaymentsError> {
     validate(amounts, Some(dates))?;
 
-    let deltas = precalculate_deltas(&dates);
-
-    Ok(xirr_result(amounts, &deltas, rate))
+    let ref deltas = day_count_factor(&dates, day_count);
+    Ok(xnpv_result(amounts, &deltas, rate))
 }
 
 // fn smart_guess(amounts: &[f64]) -> f64 {
 //     amounts.iter().sum::<f64>() / -amounts.iter().filter(|&x| x < &0.0).sum::<f64>()
 // }
 
-fn precalculate_deltas(dates: &[DateLike]) -> Vec<f64> {
+fn day_count_factor(dates: &[DateLike], day_count: Option<DayCount>) -> Vec<f64> {
     let min_date = dates.iter().min().unwrap();
-    dates.iter().map(|d| (*d - *min_date) as f64 / 365.0).collect()
+    let dc = day_count.unwrap_or_default();
+    dates.iter().map(|d| year_fraction(&min_date, &d, dc)).collect()
 }
 
 // \sum_{i=1}^n \frac{P_i}{(1 + rate)^{(d_i - d_0)/365}}
-fn xirr_result(payments: &[f64], deltas: &[f64], rate: f64) -> f64 {
-    payments.iter().zip(deltas).map(|(p, &e)| p / (1.0 + rate).powf(e)).sum()
+fn xnpv_result(payments: &[f64], deltas: &[f64], rate: f64) -> f64 {
+    payments.iter().zip(deltas).map(|(p, &e)| p * (1.0 + rate).powf(-e)).sum()
 }
 
-// XIRR first derivative
+// XNPV first derivative
 // \sum_{i=1}^n P_i * (d_0 - d_i) / 365 * (1 + rate)^{((d_0 - d_i)/365 - 1)}}
 // simplify in order to reuse cached deltas (d_i - d_0)/365
-// \sum_{i=1}^n \frac{P_i * -(d_i - d_0) / 365}{(1 + rate)^{((d_1 - d_0)/365 + 1)}}
-fn xirr_result_deriv(payments: &[f64], deltas: &[f64], rate: f64) -> f64 {
-    payments.iter().zip(deltas).map(|(p, e)| p * -e / (1.0 + rate).powf(e + 1.0)).sum()
+// \sum_{i=1}^n \frac{P_i * -(d_i - d_0) / 365}{(1 + rate)^{((d_i - d_0)/365 + 1)}}
+fn xnpv_result_deriv(payments: &[f64], deltas: &[f64], rate: f64) -> f64 {
+    payments.iter().zip(deltas).map(|(p, e)| p * -e * (1.0 + rate).powf(-e - 1.0)).sum()
 }
diff --git a/src/core/scheduled/xnfv.rs b/src/core/scheduled/xnfv.rs
index 44dc2dc..cacde96 100644
--- a/src/core/scheduled/xnfv.rs
+++ b/src/core/scheduled/xnfv.rs
@@ -1,3 +1,4 @@
+use super::{year_fraction, DayCount};
 use crate::core::models::{validate, DateLike, InvalidPaymentsError};
 use crate::core::periodic::fv;
 
@@ -9,16 +10,27 @@ pub fn xfv(
     cash_flow_rate: f64,
     end_rate: f64,
     cash_flow: f64,
+    day_count: Option<DayCount>,
 ) -> f64 {
-    self::fv(end_rate, (end_date - start_date) as f64 / 365., 0., -1., None)
-        / self::fv(cash_flow_rate, (cash_flow_date - start_date) as f64 / 365., 0., -1., None)
-        * cash_flow
+    let dc = day_count.unwrap_or_default();
+    let yf1 = year_fraction(start_date, end_date, dc);
+    let yf2 = year_fraction(start_date, cash_flow_date, dc);
+    let fv1 = self::fv(end_rate, yf1, 0., -1., None);
+    let fv2 = self::fv(cash_flow_rate, yf2, 0., -1., None);
+    fv1 / fv2 * cash_flow
 }
 
 // http://westclintech.com/SQL-Server-Financial-Functions/SQL-Server-XNFV-function
-pub fn xnfv(rate: f64, dates: &[DateLike], amounts: &[f64]) -> Result<f64, InvalidPaymentsError> {
+pub fn xnfv(
+    rate: f64,
+    dates: &[DateLike],
+    amounts: &[f64],
+    day_count: Option<DayCount>,
+) -> Result<f64, InvalidPaymentsError> {
     validate(amounts, Some(dates))?;
-    let periods = (dates.iter().max().unwrap() - dates.iter().min().unwrap()) as f64 / 365.;
-    let pv = super::xnpv(rate, dates, amounts)?;
+    let d1 = dates.iter().min().unwrap();
+    let d2 = dates.iter().max().unwrap();
+    let periods = year_fraction(d1, d2, day_count.unwrap_or_default());
+    let pv = super::xnpv(rate, dates, amounts, None)?;
     Ok(self::fv(rate, periods, 0., -pv, None))
 }
diff --git a/src/lib.rs b/src/lib.rs
index ea7002f..badc97a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,4 @@
+use conversions::PyDayCount;
 use pyo3::prelude::*;
 use pyo3::{create_exception, exceptions, wrap_pyfunction};
 
@@ -37,41 +38,54 @@ where
 }
 
 /// Internal Rate of Return for a non-periodic cash flows.
-#[pyfunction(amounts = "None", "*", guess = "0.1", silent = "false")]
-#[pyo3(text_signature = "(dates, amounts, *, guess=0.1, silent=False)")]
+#[pyfunction]
+#[pyo3(signature = (dates, amounts=None, *, guess=0.1, silent=false, day_count=None))]
+#[pyo3(
+    text_signature = "(dates, amounts=None, *, guess=0.1, silent=False, day_count=DayCount.ACT_365F)"
+)]
 fn xirr(
     py: Python,
     dates: &PyAny,
     amounts: Option<&PyAny>,
     guess: Option<f64>,
     silent: Option<bool>,
+    day_count: Option<PyDayCount>,
 ) -> PyResult<Option<f64>> {
     let (dates, amounts) = conversions::extract_payments(dates, amounts)?;
+    let day_count = day_count.map(|x| x.try_into()).transpose()?;
+
     py.allow_threads(move || {
-        let result = core::xirr(&dates, &amounts, guess);
+        let result = core::xirr(&dates, &amounts, guess, day_count);
         fallible_float_or_none(result, silent.unwrap_or(false))
     })
 }
 
 /// Net Present Value for a non-periodic cash flows.
-#[pyfunction(amounts = "None", "*", silent = "false")]
-#[pyo3(text_signature = "(rate, dates, amounts, *, silent=False)")]
+#[pyfunction]
+#[pyo3(signature = (rate, dates, amounts=None, *, silent=false, day_count=None))]
+#[pyo3(
+    text_signature = "(rate, dates, amounts=None, *, silent=False, day_count=DayCount.ACT_365F)"
+)]
 fn xnpv(
     py: Python,
     rate: f64,
     dates: &PyAny,
     amounts: Option<&PyAny>,
     silent: Option<bool>,
+    day_count: Option<PyDayCount>,
 ) -> PyResult<Option<f64>> {
     let (dates, amounts) = conversions::extract_payments(dates, amounts)?;
+    let day_count = day_count.map(|x| x.try_into()).transpose()?;
+
     py.allow_threads(move || {
-        let result = core::xnpv(rate, &dates, &amounts);
+        let result = core::xnpv(rate, &dates, &amounts, day_count);
         fallible_float_or_none(result, silent.unwrap_or(false))
     })
 }
 
 /// Internal Rate of Return
-#[pyfunction("*", guess = "0.1", silent = "false")]
+#[pyfunction]
+#[pyo3(signature = (amounts, *, guess=0.1, silent=false))]
 #[pyo3(text_signature = "(amounts, *, guess=0.1, silent=False)")]
 fn irr(
     py: Python,
@@ -92,7 +106,8 @@ fn irr(
 /// There is a difference between numpy NPV and excel NPV.
 /// By default, npv function starts from zero (numpy compatible),
 /// but you can call it with `start_from_zero=False` parameter to make it Excel compatible.
-#[pyfunction("*", start_from_zero = "true")]
+#[pyfunction]
+#[pyo3(signature = (rate, amounts, *, start_from_zero=true))]
 #[pyo3(text_signature = "(rate, amounts, *, start_from_zero = True)")]
 fn npv(
     py: Python,
@@ -108,7 +123,8 @@ fn npv(
 }
 
 /// Future Value.
-#[pyfunction("*", pmt_at_begining = "false")]
+#[pyfunction]
+#[pyo3(signature = (rate, nper, pmt, pv, *, pmt_at_begining=false))]
 #[pyo3(text_signature = "(rate, nper, pmt, pv, *, pmt_at_begining=False)")]
 fn fv(
     py: Python,
@@ -133,7 +149,10 @@ fn nfv(py: Python, rate: f64, nper: f64, amounts: &PyAny) -> PyResult<Option<f64
 /// Future value of a cash flow between two dates.
 #[pyfunction]
 #[pyo3(
-    text_signature = "(start_date, cash_flow_date, end_date, cash_flow_rate, end_rate, cash_flow)"
+    signature = (start_date, cash_flow_date, end_date, cash_flow_rate, end_rate, cash_flow, *, day_count=None)
+)]
+#[pyo3(
+    text_signature = "(start_date, cash_flow_date, end_date, cash_flow_rate, end_rate, cash_flow, * day_count=DayCount.ACT_365F)"
 )]
 fn xfv(
     py: Python,
@@ -143,7 +162,10 @@ fn xfv(
     cash_flow_rate: f64,
     end_rate: f64,
     cash_flow: f64,
+    day_count: Option<PyDayCount>,
 ) -> PyResult<Option<f64>> {
+    let day_count = day_count.map(|x| x.try_into()).transpose()?;
+
     py.allow_threads(move || {
         Ok(float_or_none(core::xfv(
             &start_date,
@@ -152,20 +174,30 @@ fn xfv(
             cash_flow_rate,
             end_rate,
             cash_flow,
+            day_count,
         )))
     })
 }
 
 /// Net future value of a series of irregular cash flows
-#[pyfunction(amounts = "None")]
-#[pyo3(text_signature = "(rate, dates, amounts)")]
-fn xnfv(py: Python, rate: f64, dates: &PyAny, amounts: Option<&PyAny>) -> PyResult<Option<f64>> {
+#[pyfunction]
+#[pyo3(signature = (rate, dates, amounts=None, *, day_count=None))]
+#[pyo3(text_signature = "(rate, dates, amounts=None, day_count=DayCount.ACT_365F)")]
+fn xnfv(
+    py: Python,
+    rate: f64,
+    dates: &PyAny,
+    amounts: Option<&PyAny>,
+    day_count: Option<PyDayCount>,
+) -> PyResult<Option<f64>> {
     let (dates, amounts) = conversions::extract_payments(dates, amounts)?;
-    py.allow_threads(move || Ok(float_or_none(core::xnfv(rate, &dates, &amounts)?)))
+    let day_count = day_count.map(|x| x.try_into()).transpose()?;
+    py.allow_threads(move || Ok(float_or_none(core::xnfv(rate, &dates, &amounts, day_count)?)))
 }
 
 /// Present Value
-#[pyfunction(fv = "0.0", "*", pmt_at_begining = "false")]
+#[pyfunction]
+#[pyo3(signature = (rate, nper, pmt, fv=0.0, *, pmt_at_begining=false))]
 #[pyo3(text_signature = "(rate, nper, pmt, fv=0, *, pmt_at_begining=False)")]
 fn pv(
     py: Python,
@@ -179,16 +211,17 @@ fn pv(
 }
 
 /// Modified Internal Rate of Return.
-#[pyfunction("*", silent = "false")]
+#[pyfunction]
+#[pyo3(signature = (amounts, finance_rate, reinvest_rate, *, silent=false))]
 #[pyo3(text_signature = "(amounts, finance_rate, reinvest_rate, *, silent=False)")]
 fn mirr(
     py: Python,
-    values: &PyAny,
+    amounts: &PyAny,
     finance_rate: f64,
     reinvest_rate: f64,
     silent: Option<bool>,
 ) -> PyResult<Option<f64>> {
-    let values = conversions::extract_amount_series(values)?;
+    let values = conversions::extract_amount_series(amounts)?;
     py.allow_threads(move || {
         let result = core::mirr(&values, finance_rate, reinvest_rate);
         fallible_float_or_none(result, silent.unwrap_or(false))
@@ -196,7 +229,8 @@ fn mirr(
 }
 
 /// Compute the payment against loan principal plus interest.
-#[pyfunction(fv = "0.0", "*", pmt_at_begining = "false")]
+#[pyfunction]
+#[pyo3(signature = (rate, nper, pv, fv=0.0, *, pmt_at_begining=false))]
 #[pyo3(text_signature = "(rate, nper, pv, fv=0, *, pmt_at_begining=False)")]
 fn pmt(
     py: Python,
@@ -210,7 +244,8 @@ fn pmt(
 }
 
 /// Compute the interest portion of a payment.
-#[pyfunction(fv = "0.0", "*", pmt_at_begining = "false")]
+#[pyfunction]
+#[pyo3(signature = (rate, per, nper, pv, fv=0.0, *, pmt_at_begining=false))]
 #[pyo3(text_signature = "(rate, per, nper, pv, fv=0, *, pmt_at_begining=False)")]
 fn ipmt(
     rate: f64,
@@ -224,7 +259,8 @@ fn ipmt(
 }
 
 /// Compute the payment against loan principal.
-#[pyfunction(fv = "0.0", "*", pmt_at_begining = "false")]
+#[pyfunction]
+#[pyo3(signature = (rate, per, nper, pv, fv=0.0, *, pmt_at_begining=false))]
 #[pyo3(text_signature = "(rate, per, nper, pv, fv=0, *, pmt_at_begining=False)")]
 fn ppmt(
     py: Python,
@@ -239,7 +275,8 @@ fn ppmt(
 }
 
 /// Compute the number of periodic payments.
-#[pyfunction(fv = "0.0", "*", pmt_at_begining = "false")]
+#[pyfunction]
+#[pyo3(signature = (rate, pmt, pv, fv=0.0, *, pmt_at_begining=false))]
 #[pyo3(text_signature = "(rate, pmt, pv, fv=0, *, pmt_at_begining=False)")]
 fn nper(
     py: Python,
@@ -253,7 +290,8 @@ fn nper(
 }
 
 /// Compute the number of periodic payments.
-#[pyfunction(fv = "0.0", "*", pmt_at_begining = "false", guess = "0.1")]
+#[pyfunction]
+#[pyo3(signature = (nper, pmt, pv, fv=0.0, *, pmt_at_begining=false, guess=0.1))]
 #[pyo3(text_signature = "(nper, pmt, pv, fv=0, *, pmt_at_begining=False, guess=0.1)")]
 fn rate(
     py: Python,
@@ -267,8 +305,22 @@ fn rate(
     py.allow_threads(move || float_or_none(core::rate(nper, pmt, pv, fv, pmt_at_begining, guess)))
 }
 
+#[pyfunction]
+fn year_fraction(d1: core::DateLike, d2: core::DateLike, day_count: PyDayCount) -> PyResult<f64> {
+    Ok(core::year_fraction(&d1, &d2, day_count.try_into()?))
+}
+
+#[pyfunction]
+fn days_between(d1: core::DateLike, d2: core::DateLike, day_count: PyDayCount) -> PyResult<i32> {
+    Ok(core::days_between(&d1, &d2, day_count.try_into()?))
+}
+
 #[pymodule]
 pub fn pyxirr(py: Python, m: &PyModule) -> PyResult<()> {
+    m.add_class::<core::DayCount>()?;
+    m.add_function(wrap_pyfunction!(year_fraction, m)?)?;
+    m.add_function(wrap_pyfunction!(days_between, m)?)?;
+
     m.add_function(wrap_pyfunction!(pmt, m)?)?;
     m.add_function(wrap_pyfunction!(ipmt, m)?)?;
     m.add_function(wrap_pyfunction!(ppmt, m)?)?;


Test patch:
diff --git a/tests/test_conversion.rs b/tests/test_conversion.rs
index e12201b..dfebdc5 100644
--- a/tests/test_conversion.rs
+++ b/tests/test_conversion.rs
@@ -123,7 +123,8 @@ fn test_extract_from_numpy_arrays() {
     let input = "tests/samples/unordered.csv";
     let result: f64 = Python::with_gil(|py| {
         let locals = get_locals(py, Some(&["numpy"]));
-        let dates = py.eval("numpy.array(dates)", Some(locals), None).unwrap();
+        let dates =
+            py.eval("numpy.array(dates, dtype='datetime64[D]')", Some(locals), None).unwrap();
         let amounts = py.eval("numpy.array(amounts)", Some(locals), None).unwrap();
         pyxirr_call!(py, "xirr", (dates, amounts))
     });
@@ -197,52 +198,49 @@ fn test_extract_from_mixed_iterables() {
 
 #[rstest]
 fn test_extract_from_non_float() {
-    let gil = Python::acquire_gil();
-    let py = gil.python();
-
-    let expected = xirr_expected_result(INPUT);
+    Python::with_gil(|py| {
+        let expected = xirr_expected_result(INPUT);
 
-    let locals = get_locals(py, Some(&["decimal"]));
-    let dates = locals.get_item("dates").unwrap();
+        let locals = get_locals(py, Some(&["decimal"]));
+        let dates = locals.get_item("dates").unwrap();
 
-    let amounts = py.eval("map(decimal.Decimal, amounts)", Some(locals), None).unwrap();
-    let result: f64 = pyxirr_call!(py, "xirr", (dates, amounts));
-    assert_almost_eq!(result, expected);
+        let amounts = py.eval("map(decimal.Decimal, amounts)", Some(locals), None).unwrap();
+        let result: f64 = pyxirr_call!(py, "xirr", (dates, amounts));
+        assert_almost_eq!(result, expected);
 
-    let amounts = py.eval("map(int, amounts)", Some(locals), None).unwrap();
-    let result: f64 = pyxirr_call!(py, "xirr", (dates, amounts));
-    assert_almost_eq!(result, expected);
+        let amounts = py.eval("map(int, amounts)", Some(locals), None).unwrap();
+        let result: f64 = pyxirr_call!(py, "xirr", (dates, amounts));
+        assert_almost_eq!(result, expected);
 
-    let amounts = py.eval("map(str, amounts)", Some(locals), None).unwrap();
-    let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err();
-    assert!(err.is_instance(py, py.get_type::<exceptions::PyTypeError>()));
+        let amounts = py.eval("map(str, amounts)", Some(locals), None).unwrap();
+        let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err();
+        assert!(err.is_instance(py, py.get_type::<exceptions::PyTypeError>()));
+    })
 }
 
 #[rstest]
 fn test_payments_different_sign() {
-    let gil = Python::acquire_gil();
-    let py = gil.python();
-
-    let locals = get_locals(py, None);
-    let dates = locals.get_item("dates").unwrap();
+    Python::with_gil(|py| {
+        let locals = get_locals(py, None);
+        let dates = locals.get_item("dates").unwrap();
 
-    let amounts = py.eval("(abs(x) for x in amounts)", Some(locals), None).unwrap();
-    let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err();
-    assert!(err.is_instance(py, py.get_type::<pyxirr::InvalidPaymentsError>()));
+        let amounts = py.eval("(abs(x) for x in amounts)", Some(locals), None).unwrap();
+        let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err();
+        assert!(err.is_instance(py, py.get_type::<pyxirr::InvalidPaymentsError>()));
 
-    let amounts = py.eval("(-abs(x) for x in amounts)", Some(locals), None).unwrap();
-    let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err();
-    assert!(err.is_instance(py, py.get_type::<pyxirr::InvalidPaymentsError>()));
+        let amounts = py.eval("(-abs(x) for x in amounts)", Some(locals), None).unwrap();
+        let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err();
+        assert!(err.is_instance(py, py.get_type::<pyxirr::InvalidPaymentsError>()));
+    })
 }
 
 #[rstest]
 fn test_arrays_of_dirrerent_lengths() {
-    let gil = Python::acquire_gil();
-    let py = gil.python();
-
-    let locals = get_locals(py, None);
-    let dates = locals.get_item("dates").unwrap();
-    let amounts = py.eval("amounts[:-2]", Some(locals), None).unwrap();
-    let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err();
-    assert!(err.is_instance(py, py.get_type::<pyxirr::InvalidPaymentsError>()));
+    Python::with_gil(|py| {
+        let locals = get_locals(py, None);
+        let dates = locals.get_item("dates").unwrap();
+        let amounts = py.eval("amounts[:-2]", Some(locals), None).unwrap();
+        let err = pyxirr_call_impl!(py, "xirr", (dates, amounts)).unwrap_err();
+        assert!(err.is_instance(py, py.get_type::<pyxirr::InvalidPaymentsError>()));
+    })
 }
diff --git a/tests/test_scheduled.rs b/tests/test_scheduled.rs
index 3767e4f..3125254 100644
--- a/tests/test_scheduled.rs
+++ b/tests/test_scheduled.rs
@@ -161,3 +161,23 @@ fn test_sum_xfv_eq_xnfv() {
         assert_almost_eq!(xnfv_result, sum_xfv_result);
     });
 }
+
+// https://www.mathworks.com/help/finance/xirr.html
+#[rstest]
+#[case("30/360 SIA", 0.100675477282743)] // 1
+#[case("act/360", 0.0991988898057063)] // 2
+#[case("act/365F", 0.10064378342638)] // 3
+#[case("30/360 ISDA", 0.100675477282743)] // 5
+#[case("30E/360", 0.100675477282743)] // 6
+#[case("act/act ISDA", 0.100739648987346)] // 12
+fn test_xirr_day_count(#[case] day_count: &str, #[case] expected: f64) {
+    Python::with_gil(|py| {
+        let dates = ["01/12/2007", "02/14/2008", "03/03/2008", "06/14/2008", "12/01/2008"];
+        let amounts = [-10000, 2500, 2000, 3000, 4000];
+
+        let kwargs = py_dict!(py, "day_count" => day_count);
+        let value: f64 = pyxirr_call!(py, "xirr", (dates, amounts), kwargs);
+
+        assert_almost_eq!(value, expected);
+    })
+}