Skip to content

Commit f62af7f

Browse files
Code refactoring
1 parent 8d2dd11 commit f62af7f

File tree

98 files changed

+36710
-177
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+36710
-177
lines changed

Makefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,12 @@ rebuild-venv: clean
136136
$(UV) venv --python $(PYTHON)
137137
$(UV) pip install ".[dev]" -e .
138138
$(UV) lock
139+
140+
.PHONY: serve-docs
141+
serve-docs:
142+
$(UV) run mkdocs serve
143+
144+
# Deploy docs to GitHub pages
145+
.PHONY: deploy-docs
146+
deploy-docs:
147+
$(UV) run mkdocs gh-deploy

README.md

Lines changed: 99 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,142 @@
1-
# Curo
1+
# Curo Python
22

3-
![Build Status](https://github.com/andrewmurphy353/curo/actions/workflows/dart_ci.yml/badge.svg)
3+
![Build Status](https://github.com/andrewmurphy353/curo_python/actions/workflows/ci.yml/badge.svg)
44
[![codecov](https://codecov.io/gh/andrewmurphy353/curo_python/branch/main/graph/badge.svg?token=YOLLETTV0K)](https://codecov.io/gh/andrewmurphy353/curo_python)
55
![GitHub License](https://img.shields.io/github/license/andrewmurphy353/curo_python?logo=github)
66

7+
Curo Python is a powerful, open-source library for performing instalment credit financial calculations, from simple loans to complex leasing and hire purchase agreements. Built from the ground up in Python, it leverages pandas DataFrames for cash flow management and SciPy for efficient solving of unknown values or rates using Brent's method.
78

8-
Introducing Curo Python, a comprehensive library designed for performing both simple and complex instalment credit financial calculations. Explore its capabilities in action with **[Curo Calculator](https://curocalc.app)**, an application built on the Curo Dart library.
9+
Explore the [documentation](https://andrewmurphy353.github.io/curo_python/), or try it in action with the [Curo Calculator](https://curocalc.app), a Flutter app showcasing similar functionality.
910

10-
...add comment that this is a complete rewrite of Curo Dart to leverage the Python pandas and Scipy libraries (add after first sentence)
11+
## Why Curo Python?
1112

12-
## Overview
13+
Curo Python is designed for developers and financial professionals who need robust tools for fixed-term credit calculations. It goes beyond basic financial algebra, offering features typically found in commercial software, such as:
1314

14-
This financial calculator library is for solving unknown cash flow values and unknown interest rates implicit in fixed-term instalment credit products, for example leasing, loans and hire purchase contracts [^1], and incorporates features that are likely to be found only in commercially developed software. It has been designed for use in applications with requirements that extend beyond what can be achieved using standard financial algebra.
15+
- Solving for unknown cash flow values (e.g., instalment amounts).
16+
- Calculating implicit interest rates (e.g., IRR or APR).
17+
- Support for multiple day count conventions, including EU and US regulatory standards.
18+
- Flexible handling of cash flow series for loans, leases, and investment scenarios.
1519

16-
For an introduction to many of the features be sure to check out the GitHub repository [examples](https://github.com/andrewmurphy353/curo_python/tree/main/example) and the accompanying cash flow diagrams which pictorially represent the cash inflows and outflows [^2] of each example.
20+
Check out the [examples folder](https://github.com/andrewmurphy353/curo_python/tree/main/examples) for practical use cases and accompanying cash flow diagrams that visualize inflows and outflows.
1721

18-
Using the calculator is straightforward as the following examples demonstrate.
22+
## Getting Started
1923

20-
### Example using `solve_value(...)` to find an unknown cash flow value:
24+
### Installation
25+
26+
Install Curo Python using your preferred package manager:
27+
28+
With **pip**:
29+
```shell
30+
$ pip install --user curo
31+
```
32+
With **uv**:
33+
```shell
34+
$ uv add curo
35+
```
36+
37+
### Basic Usage
38+
39+
Curo Python makes financial calculations intuitive. Below are two examples demonstrating how to solve for an unknown cash flow value and an implicit interest rate.
40+
41+
**Example 1: Solving for an Unknown Cash Flow Value**
42+
43+
Calculate the monthly instalment for a $10,000 loan over 6 months at an 8.25% annual interest rate.
2144

2245
```python
23-
# Step 1: Instantiate the calculator
46+
from curo import Calculator, SeriesAdvance, SeriesPayment, Mode, US30360
47+
48+
# Step 1: Create a calculator instance
2449
calculator = Calculator()
2550

26-
# Step 2: Define the advance, payment, and/or charge cash flow series
51+
# Step 2: Define cash flow series
2752
calculator.add(
28-
SeriesAdvance(
29-
label = 'Loan',
30-
amount = 10000.0,
31-
))
53+
SeriesAdvance(label="Loan", amount=10000.0)
54+
)
3255
calculator.add(
33-
SeriesPayment(
34-
numberOf = 6,
35-
label = "Instalment",
36-
amount = None, # leave undefined or None when it is the unknown to solve
37-
mode = Mode.ARREAR,
38-
))
39-
40-
# 3. Calculate the unknown cash flow value (result = 1707.00 to 2 decimal places)
41-
value_result = calculator.solve_value(
42-
day_count = US30360(),
43-
interest_rate = 0.0825)
56+
SeriesPayment(
57+
number_of=6,
58+
label="Instalment",
59+
amount=None, # Set to None for unknown value
60+
mode=Mode.ARREAR
61+
)
62+
)
63+
64+
# Step 3: Solve for the instalment amount
65+
result = calculator.solve_value(
66+
day_count=US30360(),
67+
interest_rate=0.0825
68+
)
69+
print(f"Monthly instalment: ${result:.2f}") # Output: $1707.00
4470
```
45-
In the 2nd step we set the payment series value to `None`. We could also simply omit the amount attribute. This is how the unknown cash flow values that are to be computed are identified, and is the protocol to be followed when defining the unknown cash flow values you wish to calculate.
4671

47-
In the 3rd and final step we invoke the `solve_value(...)` method, passing in a day count convention instance and the annual interest rate to use in the calculation, expressed as a decimal.
72+
**Example 2: Solving for the Implicit Interest Rate**
4873

49-
The various day count conventions available in this library are described in more detail below.
74+
Find the internal rate of return (IRR) for a €10,000 loan repaid in 6 monthly instalments of €1,707.
5075

51-
### Example using `solve_rate(...)` to find the implicit interest rate in a cash flow series:
76+
```python
77+
from curo import Calculator, SeriesAdvance, SeriesPayment, Mode, US30360, EU200848EC
5278

53-
```Python
54-
# Step 1: Instantiate the calculator
79+
# Step 1: Create a calculator instance
5580
calculator = Calculator()
5681

57-
# Step 2: Define the advance, payment, and/or charge cash flow series
82+
# Step 2: Define cash flow series
5883
calculator.add(
59-
SeriesAdvance(
60-
label = 'Loan',
61-
value = 10000.0,
62-
))
84+
SeriesAdvance(label="Loan", amount=10000.0)
85+
)
6386
calculator.add(
64-
SeriesPayment(
65-
numberOf = 6,
66-
label = 'Instalment',
67-
value = 1707.00,
68-
mode = Mode.ARREAR,
69-
))
70-
71-
# 3. Calculate the IRR or Internal Rate of Return (result = 8.250040%)
72-
irr_rate = calculator.solve_rate(
73-
dayCount = const US30360())
74-
75-
# ...or the APR for regulated EU Consumer Credit agreements (result = 8.569257%)
76-
apr_rate = calculator.solve_rate(
77-
dayCount = const EU200848EC())
87+
SeriesPayment(
88+
number_of=6,
89+
label="Instalment",
90+
amount=1707.00,
91+
mode=Mode.ARREAR
92+
)
93+
)
94+
95+
# Step 3: Calculate the IRR and APR
96+
irr = calculator.solve_rate(day_count=US30360())
97+
apr = calculator.solve_rate(day_count=EU200848EC())
98+
99+
print(f"IRR: {irr * 100:.6f}%") # Output: 8.250040%
100+
print(f"APR: {apr * 100:.6f}%") # Output: 8.569257%
78101
```
79102

103+
## Key Features
104+
80105
### Day Count Conventions
81106

82-
A day count convention is a key component of every financial calculation as it determines the method to be used in measuring the time interval between each cash flow in a series.
107+
Day count conventions determine how time intervals between cash flows are measured. Curo Python supports a wide range of conventions to meet global financial standards:
83108

84-
There are dozens of convention's defined but the more important ones supported by this calculator are as follows:
109+
Convention|Description
110+
:---------|:----------
111+
Actual ISDA | Uses actual days, accounting for leap and non-leap year portions.
112+
Actual/360 | Counts actual days, assuming a 360-day year.
113+
Actual/365 | Counts actual days, assuming a 365-day year.
114+
EU 30/360 | Assumes 30-day months and a 360-day year, per EU standards.
115+
EU 2023/2225 | Compliant with EU Directive 2023/2225 for APR calculations in consumer credit.
116+
UK CONC App | Supports UK APRC calculations for consumer credit, secured or unsecured.
117+
US 30/360 | Default for many US calculations, using 30-day months and a 360-day year.
118+
US 30U/360 | Like US 30/360, but treats February days uniformly as 30 days.
119+
US Appendix J | Implements US Regulation Z, Appendix J for APR in closed-end credit.
85120

86-
Convention | Description
87-
-----------| -------------
88-
Actual ISDA | Convention accounts for actual days between cash flow dates based on the portion in a leap year and the portion in a non-leap year as [documented here](https://en.wikipedia.org/wiki/Day_count_convention#Actual/Actual_ISDA).
89-
Actual/360 | Convention accounts for actual days between cash flow dates and considers a year to have 360 days as [documented here](https://en.wikipedia.org/wiki/Day_count_convention#Actual/360)
90-
Actual/365 | Convention accounts for actual days between cash flow dates and considers a year to have 365 days as [documented here](https://en.wikipedia.org/wiki/Day_count_convention#Actual/365_Fixed).
91-
EU 30/360 | Convention accounts for days between cash flow dates based on a 30 day month, 360 day year as [documented here](https://en.wikipedia.org/wiki/Day_count_convention#30E/360).
92-
EU 2023/2225| Convention based on the time periods between cash flow dates and the initial drawdown date, expressed in days and/or whole weeks, months or years. This convention is used specifically in APR (Annual Percentage Rate) consumer credit calculations within the European Union and is compliant with Directive (EU) 2023/2225 [available here](https://eur-lex.europa.eu/eli/dir/2023/2225/oj/eng), and is backward compatible with European Union Directive 2008/48/EC since repealed.
93-
UK CONC App (1.1 & 1.2) | Convention is used in the United Kingdom (UK) for computing the Annual Percentage Rate of Charge (APRC) for consumer credit agreements, under the Financial Services and Markets Act 2000 (FSMA 2000). This implementation supports two contexts based on whether borrowings are **secured on land** (see [FCA Handbook - CONC App 1.1](https://www.handbook.fca.org.uk/handbook/CONC/App/1/1.html)), or **not secured on land** (see [FCA Handbook - CONC App 1.2](https://www.handbook.fca.org.uk/handbook/CONC/App/1/2.html)). Refer to the class documentation for details on the day count rules.
94-
US 30/360 | Convention accounts for days between cash flow dates based on a 30 day month, 360 day year as [documented here](https://en.wikipedia.org/wiki/Day_count_convention#30/360_US). This is the default convention used by the Hewlett Packard HP12C and similar financial calculators, so choose this convention when unsure as it is the defacto convention used in the majority of fixed-term credit calculations.
95-
US 30U/360 | Convention accounts for days between cash flow dates as per US 30/360, except for the month of February where the 28th, and 29th in a leap-year, are treated as 30 days. Note, the use of U in the naming signifies Uniform, so can be read as 30 uniform days in a month.
96-
US Appendix J | Convention implements the U.S. Regulation Z, Appendix J (Federal Calendar) day count method for calculating the APR for closed-end credit transactions, such as mortgages, under the Truth in Lending Act (TILA). It uses a 30-day month divisor for odd days and supports multiple unit-periods (monthly, weekly, daily, fortnightly), including leap year handling (e.g., February 29). Time intervals are computed as whole unit-periods ((t)) plus a fractional adjustment ((f)) for odd days, aligning with the discounting formula ( P_x / (1 + f \times i) (1 + i)^t ). Results are validated against the [FFIEC APR Calculator](https://www.ffiec.gov/examtools/FFIEC-Calculators/APR/#/accountdata). See [12 CFR Part 1026, Appendix J](https://www.ecfr.gov/current/title-12/chapter-X/part-1026/appendix-Appendix%20J%20to%20Part%201026) for details.
121+
For XIRR-style calculations (referencing the first drawdown date), pass `use_xirr_method=True` to the convention constructor. When used with `Actual/365`, this matches Microsoft Excel’s XIRR function.
97122

98-
All conventions, except EU 2023/2225, UK CONC App 1.1 and 1.2, and US Appendix J, will by default compute time intervals between cash flows with reference to the dates of adjacent cash flows.
123+
### Cash Flow Diagrams
99124

100-
To override this so that time intervals are computed with reference to the first drawdown date, as in XIRR (eXtended Internal Rate of Return) based calculations, simply pass `use_xirr_method = True` to the respective day count convention constructor (refer to the code documentation for details).
125+
Cash flow diagrams visually represent the timing and direction of financial transactions. For example, a €10,000 loan repaid in 6 monthly instalments would look like this:
101126

102-
When the Actual/365 convention is used in this manner, e.g. `Act365(use_xirr_method = True)` the XIRR result will equal that produced by the equivalent Microsoft Excel XIRR function.
127+
![Cash Flow Diagram](https://github.com/andrewmurphy353/curo_python/raw/main/assets/images/cash_flow_diagram_01.png)
103128

104-
## Installation
105-
106-
With pip
107-
```shell
108-
$ pip install --user curo
109-
```
110-
With uv
111-
```shell
112-
$ uv add curo
113-
```
129+
- **Down arrows**: Money received (e.g., loan advance).
130+
- **Up arrows**: Money paid (e.g., instalments).
131+
- **Time line**: Represents the contract term, divided into compounding periods.
114132

115133
## License
116134

117135
Copyright © 2026, [Andrew Murphy](https://github.com/andrewmurphy353).
118136
Released under the [MIT License](LICENSE).
119137

120-
### Footnotes
121-
---
122-
123-
[^1] Whilst the library uses asset finance nomenclature, it is equally capable of solving problems in investment-type scenarios.
124-
125-
[^2] A cash flow diagram is simply a pictorial representation of the timing and direction of financial transactions.
126-
127-
The diagram begins with a horizontal line, called a time line. The line represents the duration or contract term, and is commonly divided into compounding periods. The exchange of money in the financial arrangement is depicted by vertical arrows. Money a lender receives is represented by an arrow pointing up from the point in the time line when the transaction occurs; money paid out by the lender is represented by an arrow pointing down. The collection of all up and down arrow cash flows are what is referred to throughout the calculator documentation as a cash flow series.
128-
129-
To illustrate using the example above, that is a 10,000.00 loan repayable by 6 monthly instalments in arrears (due at the end of each compounding period), the cash flow diagram would resemble something like this:
138+
## Learn More
130139

131-
![image](https://github.com/andrewmurphy353/curo_python/raw/main/assets/images/cash_flow_diagram_01.png)
140+
- **Examples**: Dive into practical use cases in the [examples folder](https://github.com/andrewmurphy353/curo_python/tree/main/examples).
141+
- **Documentation**: Refer to the code [documentation](https://andrewmurphy353.github.io/curo_python/) for detailed class and method descriptions.
142+
- **Issues & Contributions**: Report bugs or contribute on [GitHub](https://github.com/andrewmurphy353/curo_python/issues).

curo/calculator.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ def add(self, series: Series) -> None:
8484
ValidationError: If a bespoke profile is set, as series cannot be added in this mode.
8585
8686
Notes:
87-
- The order of addition matters for `undated` series, as their cash flow dates
88-
are inferred from the order in the series list, with later additions following
89-
previous ones.
90-
- `Dated` series use their provided start date and are unaffected by order.
91-
- If `series.amount` is not None, it is rounded to the Calculator's precision.
87+
- The order of addition matters for `undated` series, as their cash flow dates
88+
are inferred from the order in the series list, with later additions following
89+
previous ones.
90+
- `Dated` series use their provided start date and are unaffected by order.
91+
- If `series.amount` is not None, it is rounded to the Calculator's precision.
9292
"""
9393
if self._is_bespoke_profile:
9494
raise ValidationError("Cannot add series with a bespoke profile")
@@ -121,13 +121,14 @@ def solve_value(
121121
UnsolvableError: If no amount can be found to achieve NFV = 0.
122122
123123
Notes:
124-
Uses scipy.optimize.brentq to find the base amount where NFV = 0.
125-
Supports bespoke profiles (self.profile) or series-based profiles (self._series).
126-
For USAppendixJ, converts the interest rate to periodic.
127-
The returned value is the raw amount before applying weightings. For weighted
128-
payments, multiply by each series' `weighting` to get the final amount for
129-
amortization or APR schedules. Updates self.profile with solved amounts and,
130-
if not use_xirr_method, amortizes interest.
124+
- Uses scipy.optimize.brentq to find the base amount where NFV = 0.
125+
- Supports bespoke profiles (self.profile) or series-based profiles (self._series).
126+
- For USAppendixJ, converts the interest rate to periodic.
127+
- The returned value is the raw amount before applying weightings. For weighted
128+
payments, multiply by each series' `weighting` to get the final amount for
129+
or APR schedules.
130+
- Updates self.profile with solved amounts and, if not use_xirr_method,
131+
amortizes interest.
131132
"""
132133
# Build, sort, and validate cash flow profile
133134
if self._is_bespoke_profile:
@@ -211,10 +212,10 @@ def solve_rate(
211212
UnsolvableError: If no rate can be found within bounds.
212213
213214
Notes:
214-
Uses scipy.optimize.brentq for root-finding within [-0.9999, upper_bound].
215-
Upper bound is configurable to handle extreme APRs (e.g., usurious loans > 1000%).
216-
Very large upper bounds (e.g., > 100.0) may slow convergence or cause numerical issues.
217-
For USAppendixJ, the periodic rate is annualized by multiplying by periods_in_year.
215+
- Uses scipy.optimize.brentq for root-finding within [-0.9999, upper_bound].
216+
- Upper bound is configurable to handle extreme APRs (e.g., usurious loans > 1000%).
217+
- Very large upper bounds (e.g., > 100.0) may slow convergence or cause numerical issues.
218+
- For USAppendixJ, the periodic rate is annualized by multiplying by periods_in_year.
218219
"""
219220
if upper_bound <= 0.0:
220221
raise ValidationError("Upper bound must be positive")

curo/daycount/actual_360.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Actual360(Convention):
2222
use_xirr_method: If True, uses the XIRR method, setting day count origin to
2323
DRAWDOWN; if False, uses NEIGHBOUR. Defaults to False.
2424
25-
Example:
25+
Examples:
2626
>>> dc = Actual360()
2727
>>> factor = dc.compute_factor(
2828
... pd.Timestamp('2020-01-28', tz='UTC'),
@@ -57,7 +57,7 @@ def compute_factor(self, start: pd.Timestamp, end: pd.Timestamp) -> DayCountFact
5757
Raises:
5858
ValueError: If `end` is before `start`.
5959
60-
Example:
60+
Examples:
6161
>>> dc = Actual360()
6262
>>> factor = dc.compute_factor(
6363
... pd.Timestamp('2020-01-28', tz='UTC'),

curo/daycount/actual_365.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Actual365(Convention):
2222
use_xirr_method: If True, uses the XIRR method, setting day count origin to
2323
DRAWDOWN; if False, uses NEIGHBOUR. Defaults to False.
2424
25-
Example:
25+
Examples:
2626
>>> dc = Actual365()
2727
>>> factor = dc.compute_factor(
2828
... pd.Timestamp('2020-01-28', tz='UTC'),
@@ -57,7 +57,7 @@ def compute_factor(self, start: pd.Timestamp, end: pd.Timestamp) -> DayCountFact
5757
Raises:
5858
ValueError: If `end` is before `start`.
5959
60-
Example:
60+
Examples:
6161
>>> dc = Actual365()
6262
>>> factor = dc.compute_factor(
6363
... pd.Timestamp('2020-01-28', tz='UTC'),

0 commit comments

Comments
 (0)