diff --git a/.auto-claude-security.json b/.auto-claude-security.json
new file mode 100644
index 00000000..1712c7eb
--- /dev/null
+++ b/.auto-claude-security.json
@@ -0,0 +1,183 @@
+{
+ "base_commands": [
+ ".",
+ "[",
+ "[[",
+ "ag",
+ "awk",
+ "basename",
+ "bash",
+ "bc",
+ "break",
+ "cat",
+ "cd",
+ "chmod",
+ "clear",
+ "cmp",
+ "column",
+ "comm",
+ "command",
+ "continue",
+ "cp",
+ "curl",
+ "cut",
+ "date",
+ "df",
+ "diff",
+ "dig",
+ "dirname",
+ "du",
+ "echo",
+ "egrep",
+ "env",
+ "eval",
+ "exec",
+ "exit",
+ "expand",
+ "export",
+ "expr",
+ "false",
+ "fd",
+ "fgrep",
+ "file",
+ "find",
+ "fmt",
+ "fold",
+ "gawk",
+ "gh",
+ "git",
+ "grep",
+ "gunzip",
+ "gzip",
+ "head",
+ "help",
+ "host",
+ "iconv",
+ "id",
+ "jobs",
+ "join",
+ "jq",
+ "kill",
+ "killall",
+ "less",
+ "let",
+ "ln",
+ "ls",
+ "lsof",
+ "man",
+ "mkdir",
+ "mktemp",
+ "more",
+ "mv",
+ "nl",
+ "paste",
+ "pgrep",
+ "ping",
+ "pkill",
+ "popd",
+ "printenv",
+ "printf",
+ "ps",
+ "pushd",
+ "pwd",
+ "read",
+ "readlink",
+ "realpath",
+ "reset",
+ "return",
+ "rev",
+ "rg",
+ "rm",
+ "rmdir",
+ "sed",
+ "seq",
+ "set",
+ "sh",
+ "shuf",
+ "sleep",
+ "sort",
+ "source",
+ "split",
+ "stat",
+ "tail",
+ "tar",
+ "tee",
+ "test",
+ "time",
+ "timeout",
+ "touch",
+ "tr",
+ "tree",
+ "true",
+ "type",
+ "uname",
+ "unexpand",
+ "uniq",
+ "unset",
+ "unzip",
+ "watch",
+ "wc",
+ "wget",
+ "whereis",
+ "which",
+ "whoami",
+ "xargs",
+ "yes",
+ "yq",
+ "zip",
+ "zsh"
+ ],
+ "stack_commands": [
+ "composer",
+ "ipython",
+ "jupyter",
+ "node",
+ "notebook",
+ "npm",
+ "npx",
+ "pdb",
+ "php",
+ "phpunit",
+ "pip",
+ "pip3",
+ "pipx",
+ "pudb",
+ "python",
+ "python3"
+ ],
+ "script_commands": [
+ "./parallel.sh"
+ ],
+ "custom_commands": [],
+ "detected_stack": {
+ "languages": [
+ "python",
+ "javascript",
+ "php"
+ ],
+ "package_managers": [
+ "composer"
+ ],
+ "frameworks": [
+ "phpunit"
+ ],
+ "databases": [],
+ "infrastructure": [],
+ "cloud_providers": [],
+ "code_quality_tools": [],
+ "version_managers": []
+ },
+ "custom_scripts": {
+ "npm_scripts": [],
+ "make_targets": [],
+ "poetry_scripts": [],
+ "cargo_aliases": [],
+ "shell_scripts": [
+ "parallel.sh"
+ ]
+ },
+ "project_dir": "/home/jordan/Projects/Fermat",
+ "created_at": "2026-01-17T02:59:58.884479",
+ "project_hash": "ff04866add36ffa1116480549ed508fe",
+ "inherited_from": "/home/jordan/Projects/Fermat"
+}
\ No newline at end of file
diff --git a/.auto-claude-status b/.auto-claude-status
new file mode 100644
index 00000000..ef719047
--- /dev/null
+++ b/.auto-claude-status
@@ -0,0 +1,25 @@
+{
+ "active": true,
+ "spec": "003-financial-mathematics-module",
+ "state": "building",
+ "subtasks": {
+ "completed": 18,
+ "total": 20,
+ "in_progress": 1,
+ "failed": 0
+ },
+ "phase": {
+ "current": "Integration and Full Test Suite",
+ "id": null,
+ "total": 3
+ },
+ "workers": {
+ "active": 0,
+ "max": 1
+ },
+ "session": {
+ "number": 20,
+ "started_at": "2026-01-17T18:00:38.026136"
+ },
+ "last_update": "2026-01-17T18:48:06.189166"
+}
\ No newline at end of file
diff --git a/.claude_settings.json b/.claude_settings.json
new file mode 100644
index 00000000..318f5f8d
--- /dev/null
+++ b/.claude_settings.json
@@ -0,0 +1,39 @@
+{
+ "sandbox": {
+ "enabled": true,
+ "autoAllowBashIfSandboxed": true
+ },
+ "permissions": {
+ "defaultMode": "acceptEdits",
+ "allow": [
+ "Read(./**)",
+ "Write(./**)",
+ "Edit(./**)",
+ "Glob(./**)",
+ "Grep(./**)",
+ "Read(/home/jordan/Projects/Fermat/.auto-claude/worktrees/tasks/003-financial-mathematics-module/**)",
+ "Write(/home/jordan/Projects/Fermat/.auto-claude/worktrees/tasks/003-financial-mathematics-module/**)",
+ "Edit(/home/jordan/Projects/Fermat/.auto-claude/worktrees/tasks/003-financial-mathematics-module/**)",
+ "Glob(/home/jordan/Projects/Fermat/.auto-claude/worktrees/tasks/003-financial-mathematics-module/**)",
+ "Grep(/home/jordan/Projects/Fermat/.auto-claude/worktrees/tasks/003-financial-mathematics-module/**)",
+ "Read(/home/jordan/Projects/Fermat/.auto-claude/worktrees/tasks/003-financial-mathematics-module/.auto-claude/specs/003-financial-mathematics-module/**)",
+ "Write(/home/jordan/Projects/Fermat/.auto-claude/worktrees/tasks/003-financial-mathematics-module/.auto-claude/specs/003-financial-mathematics-module/**)",
+ "Edit(/home/jordan/Projects/Fermat/.auto-claude/worktrees/tasks/003-financial-mathematics-module/.auto-claude/specs/003-financial-mathematics-module/**)",
+ "Read(/home/jordan/Projects/Fermat/.auto-claude/**)",
+ "Write(/home/jordan/Projects/Fermat/.auto-claude/**)",
+ "Edit(/home/jordan/Projects/Fermat/.auto-claude/**)",
+ "Glob(/home/jordan/Projects/Fermat/.auto-claude/**)",
+ "Grep(/home/jordan/Projects/Fermat/.auto-claude/**)",
+ "Bash(*)",
+ "WebFetch(*)",
+ "WebSearch(*)",
+ "mcp__context7__resolve-library-id(*)",
+ "mcp__context7__get-library-docs(*)",
+ "mcp__graphiti-memory__search_nodes(*)",
+ "mcp__graphiti-memory__search_facts(*)",
+ "mcp__graphiti-memory__add_episode(*)",
+ "mcp__graphiti-memory__get_episodes(*)",
+ "mcp__graphiti-memory__get_entity_edge(*)"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 9af31763..91fe11b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,13 @@
-.idea
-vendor/
-composer.lock
-build
-.phpunit.result.cache
-site
-tools/
-.php-cs-fixer.cache
-.phive/
-phive.phar.asc
\ No newline at end of file
+.idea
+vendor/
+composer.lock
+build
+.phpunit.result.cache
+site
+tools/
+.php-cs-fixer.cache
+.phive/
+phive.phar.asc
+
+# Auto Claude data directory
+.auto-claude/
diff --git a/docs/getting-started/modules.md b/docs/getting-started/modules.md
index 42f370ea..cc3d27c9 100644
--- a/docs/getting-started/modules.md
+++ b/docs/getting-started/modules.md
@@ -62,6 +62,22 @@ This is unlikely to happen in most applications however, unless your application
!!! note "See Also"
The [Complex Numbers](../modules/complex-numbers/about.md) section of this documentation contains more detailed information about this module and its behavior.
+### Fermat Finance
+
+!!! caution "Unstable"
+ The Fermat Finance module does **not** have a released stable version and should not be used in a production environment.
+
+ To include this module anyway, require the current development state like so:
+
+ `composer require "samsara/fermat-finance:dev-master"`
+
+The Fermat Finance module provides comprehensive financial mathematics calculations including time value of money, loan analysis, investment evaluation, and bond pricing. This module has no other dependencies and can be required as a stand-alone addition to Fermat.
+
+Financial calculations often involve complex iterative algorithms (like IRR and bond yield calculations), so the scale setting on your objects tends to have an impact on performance within this module.
+
+!!! see-also "See Also"
+ The [Finance](../modules/finance/about.md) section of this documentation contains more detailed information about this module and its behavior.
+
### Fermat Linear Algebra
!!! caution "Unstable"
diff --git a/docs/modules/finance/about.md b/docs/modules/finance/about.md
new file mode 100644
index 00000000..6bdb3c20
--- /dev/null
+++ b/docs/modules/finance/about.md
@@ -0,0 +1,212 @@
+# The Fermat Finance Module
+
+This module provides various mathematical objects and functions to help a program perform complex financial calculations, most often useful in banking, investment, accounting, and financial analysis applications. Using the time value of money calculations, it becomes easy to create applications that compute loan payments and amortization schedules. Using the investment analysis functions, it becomes straightforward to evaluate the profitability of projects using NPV and IRR.
+
+While many programs don't see a benefit from utilizing financial mathematics operations and functions, the ones that do benefit from it immensely.
+
+## Key Concepts
+
+This section details the key concepts of the library to understand how to create your program using it. This does *not* cover the key mathematical concepts within finance that this program implements.
+
+### FinanceProvider
+
+The `FinanceProvider` contains all financial calculation static functions that allow direct arbitrary precision calculation of the most common formulas in financial mathematics. All functions accept inputs as `int`, `float`, `string`, or `Decimal` objects and return `ImmutableDecimal` results for maximum precision.
+
+### Interest Calculations
+
+The Finance module supports both simple and compound interest calculations:
+
+**Simple Interest** follows the formula $`I = P \times r \times t`$, where P is the principal, r is the interest rate, and t is time. This is useful for short-term financial instruments and basic interest calculations.
+
+**Compound Interest** is calculated using the formula $`A = P(1 + \frac{r}{n})^{nt}`$ for regular compounding, where n is the compounding frequency. For continuous compounding (when n = 0), the formula becomes $`A = Pe^{rt}`$. Compound interest is essential for modeling most real-world savings accounts, investments, and loans.
+
+Example usage:
+```php
+use Samsara\Fermat\Finance\Provider\FinanceProvider;
+
+// Calculate compound interest: $1000 at 5% annual rate for 10 years, compounded quarterly
+$futureValue = FinanceProvider::compoundInterest(
+ principal: 1000,
+ rate: 0.05,
+ time: 10,
+ frequency: 4 // Quarterly compounding
+);
+// Returns approximately $1643.62
+
+// Calculate continuous compounding
+$continuousValue = FinanceProvider::compoundInterest(
+ principal: 1000,
+ rate: 0.05,
+ time: 10,
+ frequency: 0 // Continuous compounding
+);
+// Returns approximately $1648.72
+```
+
+### Time Value of Money
+
+The core principle that money available today is worth more than the same amount in the future due to its earning potential is captured in Present Value (PV) and Future Value (FV) calculations.
+
+**Present Value** uses the formula $`PV = \frac{FV}{(1 + r)^n}`$ to determine what a future amount is worth today. This is essential for discount calculations and investment valuation.
+
+**Future Value** uses the formula $`FV = PV \times (1 + r)^n`$ to determine what a current amount will be worth in the future given a specific interest rate.
+
+Example usage:
+```php
+use Samsara\Fermat\Finance\Provider\FinanceProvider;
+
+// What is $10,000 in 5 years worth today at a 6% annual discount rate?
+$presentValue = FinanceProvider::presentValue(
+ futureValue: 10000,
+ rate: 0.06,
+ periods: 5
+);
+// Returns approximately $7472.58
+
+// What will $5,000 be worth in 3 years at 4% annual interest?
+$futureValue = FinanceProvider::futureValue(
+ presentValue: 5000,
+ rate: 0.04,
+ periods: 3
+);
+// Returns approximately $5624.32
+```
+
+### Annuities
+
+An annuity is a series of equal payments made at regular intervals. The Finance module provides calculations for both the present and future value of ordinary annuities (payments at the end of each period).
+
+**Annuity Present Value** calculates the current worth of a stream of future payments using the formula $`PV = PMT \times \frac{1 - (1 + r)^{-n}}{r}`$ for non-zero rates. This is useful for valuing pension plans, structured settlements, and other payment streams.
+
+**Annuity Future Value** calculates what a series of regular payments will grow to using the formula $`FV = PMT \times \frac{(1 + r)^n - 1}{r}`$. This is essential for retirement planning and savings goal calculations.
+
+### Loan Calculations
+
+The Finance module provides comprehensive loan analysis capabilities:
+
+**Loan Payment** calculates the periodic payment required to amortize a loan using the formula $`PMT = P \times \frac{r(1 + r)^n}{(1 + r)^n - 1}`$. This is the foundation for mortgage calculators and loan analysis tools.
+
+**Amortization Schedule** generates a complete payment-by-payment breakdown showing how each payment is split between principal and interest, along with the remaining balance. This returns an array where each element contains:
+- `period`: The payment period number
+- `payment`: The total payment amount
+- `principal`: The principal portion of the payment
+- `interest`: The interest portion of the payment
+- `balance`: The remaining loan balance after the payment
+
+Example usage:
+```php
+use Samsara\Fermat\Finance\Provider\FinanceProvider;
+
+// Calculate monthly payment on a $200,000 loan at 4.5% annual rate for 30 years
+$monthlyPayment = FinanceProvider::loanPayment(
+ principal: 200000,
+ rate: 0.045 / 12, // Monthly rate
+ periods: 30 * 12 // 360 months
+);
+// Returns approximately $1013.37
+
+// Generate complete amortization schedule
+$schedule = FinanceProvider::amortizationSchedule(
+ principal: 200000,
+ rate: 0.045 / 12,
+ periods: 30 * 12
+);
+// Returns array of 360 payment details
+```
+
+### Investment Analysis
+
+For evaluating the profitability and return of investments and projects:
+
+**Net Present Value (NPV)** calculates the present value of all cash flows using the formula $`NPV = \sum_{t=0}^{n} \frac{CF_t}{(1 + r)^t}`$. A positive NPV indicates a profitable investment. The cash flows array should be indexed by period, with period 0 typically being the initial investment (negative value).
+
+**Internal Rate of Return (IRR)** finds the discount rate that makes NPV equal to zero. This represents the effective return rate of an investment. The calculation uses the Newton-Raphson iterative method for precise arbitrary precision results.
+
+Example usage:
+```php
+use Samsara\Fermat\Finance\Provider\FinanceProvider;
+
+// Evaluate a project: $100,000 initial investment with returns over 5 years
+$cashFlows = [
+ 0 => -100000, // Initial investment
+ 1 => 25000,
+ 2 => 30000,
+ 3 => 35000,
+ 4 => 30000,
+ 5 => 25000
+];
+
+// Calculate NPV at 8% discount rate
+$npv = FinanceProvider::netPresentValue($cashFlows, 0.08);
+// Returns approximately $16308.52 (profitable project)
+
+// Calculate IRR
+$irr = FinanceProvider::internalRateOfReturn($cashFlows);
+// Returns approximately 0.1436 (14.36% return)
+```
+
+### Bond Calculations
+
+For fixed-income securities analysis:
+
+**Bond Price** calculates the theoretical price of a bond based on its coupon rate, market interest rate, and time to maturity. The formula combines the present value of coupon payments (an annuity) with the present value of the face value returned at maturity.
+
+**Yield to Maturity (YTM)** calculates the total return anticipated on a bond if held until maturity. This is effectively the IRR of the bond's cash flows given its current market price. The function uses a financial approximation for the initial guess and then refines it using the Newton-Raphson method.
+
+Example usage:
+```php
+use Samsara\Fermat\Finance\Provider\FinanceProvider;
+
+// Price a $1000 face value bond with 6% coupon rate,
+// when market rate is 5%, with 10 periods until maturity
+$bondPrice = FinanceProvider::bondPrice(
+ faceValue: 1000,
+ couponRate: 0.06,
+ marketRate: 0.05,
+ periods: 10
+);
+// Returns approximately $1077.22 (premium bond, trades above par)
+
+// Calculate YTM for a bond trading at $950
+$ytm = FinanceProvider::bondYieldToMaturity(
+ currentPrice: 950,
+ faceValue: 1000,
+ couponRate: 0.06,
+ periods: 10
+);
+// Returns approximately 0.0683 (6.83% yield)
+```
+
+## Error Handling
+
+All Finance module functions include comprehensive validation and error handling:
+
+- **IntegrityConstraint Exception**: Thrown when inputs violate mathematical constraints, such as:
+ - Interest/discount rates of -100% or less (would cause division by zero)
+ - Empty cash flows arrays
+ - IRR calculations that fail to converge
+ - Zero derivatives in iterative calculations
+
+- **IncompatibleObjectState Exception**: Thrown when Decimal object operations encounter incompatible states
+
+Always ensure your interest rates, discount rates, and other financial parameters are within valid ranges for meaningful results.
+
+## Performance Considerations
+
+Financial calculations, especially iterative methods like IRR and YTM, can be computationally intensive when using arbitrary precision. The scale setting on your Decimal objects has a significant impact on performance:
+
+- For most financial applications, a scale of 2-6 decimal places is sufficient
+- Bond yield and IRR calculations may require 4-6 decimal places for accurate results
+- The `maxIterations` parameter in IRR and YTM functions can be adjusted to balance precision and performance
+- Amortization schedules for long-term loans (e.g., 30-year mortgages) generate large arrays and should be cached when possible
+
+## Integration with Fermat Core
+
+All Finance module calculations seamlessly integrate with Fermat's core number types:
+
+- Accept inputs as `int`, `float`, `string`, or any `Decimal` type
+- Return `ImmutableDecimal` results for thread-safety and precision
+- Preserve arbitrary precision throughout all calculations
+- Support chaining with other Fermat operations
+
+This allows you to combine financial calculations with other mathematical operations in Fermat without losing precision or having to convert between different numeric types.
diff --git a/docs/roster/latest/Fermat Finance/Provider/FinanceProvider.md b/docs/roster/latest/Fermat Finance/Provider/FinanceProvider.md
new file mode 100644
index 00000000..6fc6b21d
--- /dev/null
+++ b/docs/roster/latest/Fermat Finance/Provider/FinanceProvider.md
@@ -0,0 +1,698 @@
+# Samsara\Fermat\Finance\Provider > FinanceProvider
+
+*No description available*
+
+
+## Methods
+
+
+### Static Methods
+
+!!! signature "public FinanceProvider::simpleInterest(int|float|string|Decimal $principal, int|float|string|Decimal $rate, int|float|string|Decimal $time)"
+ ##### simpleInterest
+ **$principal**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The principal amount
+
+ **$rate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The interest rate (as decimal, e.g., 0.05 for 5%)
+
+ **$time**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The time period
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### simpleInterest() Description:
+
+ Calculate simple interest.
+
+ Formula: I = P * r * t
+
+---
+
+!!! signature "public FinanceProvider::compoundInterest(int|float|string|Decimal $principal, int|float|string|Decimal $rate, int|float|string|Decimal $time, int|float|string|Decimal $frequency)"
+ ##### compoundInterest
+ **$principal**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The principal amount
+
+ **$rate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The annual interest rate (as decimal, e.g., 0.05 for 5%)
+
+ **$time**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The time period in years
+
+ **$frequency**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The number of times interest is compounded per year (use 0 for continuous)
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### compoundInterest() Description:
+
+ Calculate compound interest.
+
+ Formula: A = P(1 + r/n)^(nt) for regular compounding
+ Formula: A = Pe^(rt) for continuous compounding (when n = 0)
+
+---
+
+!!! signature "public FinanceProvider::presentValue(int|float|string|Decimal $futureValue, int|float|string|Decimal $rate, int|float|string|Decimal $periods)"
+ ##### presentValue
+ **$futureValue**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The future value
+
+ **$rate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The discount rate per period (as decimal, e.g., 0.05 for 5%)
+
+ **$periods**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The number of periods
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### presentValue() Description:
+
+ Calculate present value.
+
+ Formula: PV = FV / (1 + r)^n
+
+---
+
+!!! signature "public FinanceProvider::futureValue(int|float|string|Decimal $presentValue, int|float|string|Decimal $rate, int|float|string|Decimal $periods)"
+ ##### futureValue
+ **$presentValue**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The present value
+
+ **$rate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The interest rate per period (as decimal, e.g., 0.05 for 5%)
+
+ **$periods**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The number of periods
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### futureValue() Description:
+
+ Calculate future value.
+
+ Formula: FV = PV * (1 + r)^n
+
+---
+
+!!! signature "public FinanceProvider::annuityPresentValue(int|float|string|Decimal $payment, int|float|string|Decimal $rate, int|float|string|Decimal $periods)"
+ ##### annuityPresentValue
+ **$payment**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The payment amount per period
+
+ **$rate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The interest rate per period (as decimal, e.g., 0.05 for 5%)
+
+ **$periods**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The number of periods
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### annuityPresentValue() Description:
+
+ Calculate the present value of an annuity (ordinary annuity).
+
+ Formula: PV = PMT * [(1 - (1 + r)^-n) / r] for r != 0
+ Formula: PV = PMT * n for r = 0
+
+---
+
+!!! signature "public FinanceProvider::annuityFutureValue(int|float|string|Decimal $payment, int|float|string|Decimal $rate, int|float|string|Decimal $periods)"
+ ##### annuityFutureValue
+ **$payment**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The payment amount per period
+
+ **$rate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The interest rate per period (as decimal, e.g., 0.05 for 5%)
+
+ **$periods**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The number of periods
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### annuityFutureValue() Description:
+
+ Calculate the future value of an annuity (ordinary annuity).
+
+ Formula: FV = PMT * [((1 + r)^n - 1) / r] for r != 0
+ Formula: FV = PMT * n for r = 0
+
+---
+
+!!! signature "public FinanceProvider::loanPayment(int|float|string|Decimal $principal, int|float|string|Decimal $rate, int|float|string|Decimal $periods)"
+ ##### loanPayment
+ **$principal**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The principal (loan amount)
+
+ **$rate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The interest rate per period (as decimal, e.g., 0.05 for 5%)
+
+ **$periods**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The number of periods
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### loanPayment() Description:
+
+ Calculate loan payment amount (ordinary annuity payment).
+
+ Formula: PMT = P * [r(1 + r)^n] / [(1 + r)^n - 1] for r != 0
+ Formula: PMT = P / n for r = 0
+
+---
+
+!!! signature "public FinanceProvider::amortizationSchedule(int|float|string|Decimal $principal, int|float|string|Decimal $rate, int|float|string|Decimal $periods)"
+ ##### amortizationSchedule
+ **$principal**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The principal (loan amount)
+
+ **$rate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The interest rate per period (as decimal, e.g., 0.05 for 5%)
+
+ **$periods**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The number of periods
+
+
+
+ **return**
+
+ type
+ : array
+
+ description
+ : Array of payment details for each period
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### amortizationSchedule() Description:
+
+ Generate a loan amortization schedule.
+
+ Returns an array of payment details for each period, including:
+ - period: The payment period number
+ - payment: The total payment amount
+ - principal: The principal portion of the payment
+ - interest: The interest portion of the payment
+ - balance: The remaining loan balance after the payment
+
+---
+
+!!! signature "public FinanceProvider::netPresentValue(array $cashFlows, int|float|string|Decimal $rate)"
+ ##### netPresentValue
+ **$cashFlows**
+
+ type
+ : array
+
+ description
+ : Array of cash flows indexed by period (period 0 is initial investment, typically negative)
+
+ **$rate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The discount rate per period (as decimal, e.g., 0.05 for 5%)
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### netPresentValue() Description:
+
+ Calculate net present value (NPV) of a series of cash flows.
+
+ Formula: NPV = Σ(CF_t / (1 + r)^t) for t from 0 to n
+
+---
+
+!!! signature "public FinanceProvider::internalRateOfReturn(array $cashFlows, int|float|string|Decimal $guess, int|float|string|Decimal $tolerance, int $maxIterations)"
+ ##### internalRateOfReturn
+ **$cashFlows**
+
+ type
+ : array
+
+ description
+ : Array of cash flows indexed by period (period 0 is initial investment, typically negative)
+
+ **$guess**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : Initial guess for IRR (default 0.1 for 10%)
+
+ **$tolerance**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : Convergence tolerance (default 0.000001)
+
+ **$maxIterations**
+
+ type
+ : int
+
+ description
+ : Maximum number of iterations (default 100)
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### internalRateOfReturn() Description:
+
+ Calculate the internal rate of return (IRR) for a series of cash flows.
+
+ IRR is the discount rate that makes the net present value (NPV) equal to zero.
+ Uses the Newton-Raphson method for iterative approximation.
+
+ Formula: Find r such that NPV = Σ(CF_t / (1 + r)^t) = 0
+
+---
+
+!!! signature "public FinanceProvider::bondPrice(int|float|string|Decimal $faceValue, int|float|string|Decimal $couponRate, int|float|string|Decimal $marketRate, int|float|string|Decimal $periods)"
+ ##### bondPrice
+ **$faceValue**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The face value (par value) of the bond
+
+ **$couponRate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The coupon rate per period (as decimal, e.g., 0.05 for 5%)
+
+ **$marketRate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The market interest rate (yield) per period (as decimal, e.g., 0.04 for 4%)
+
+ **$periods**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The number of periods until maturity
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### bondPrice() Description:
+
+ Calculate the price of a bond.
+
+ Formula: P = C * [(1 - (1 + r)^-n) / r] + F / (1 + r)^n for r != 0
+ Formula: P = C * n + F for r = 0
+
+---
+
+!!! signature "public FinanceProvider::bondYieldToMaturity(int|float|string|Decimal $currentPrice, int|float|string|Decimal $faceValue, int|float|string|Decimal $couponRate, int|float|string|Decimal $periods, int|float|string|Decimal $guess, int|float|string|Decimal $tolerance, int $maxIterations)"
+ ##### bondYieldToMaturity
+ **$currentPrice**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The current market price of the bond
+
+ **$faceValue**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The face value (par value) of the bond
+
+ **$couponRate**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The coupon rate per period (as decimal, e.g., 0.05 for 5%)
+
+ **$periods**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : The number of periods until maturity
+
+ **$guess**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : Initial guess for YTM (default 0.05 for 5%)
+
+ **$tolerance**
+
+ type
+ : int|float|string|Decimal
+
+ description
+ : Convergence tolerance (default 0.000001)
+
+ **$maxIterations**
+
+ type
+ : int
+
+ description
+ : Maximum number of iterations (default 100)
+
+
+
+ **return**
+
+ type
+ : Samsara\Fermat\Core\Values\ImmutableDecimal
+
+ description
+ : *No description available*
+
+ **throws**
+
+ Samsara\Exceptions\SystemError\LogicalError\IncompatibleObjectState
+ :
+
+ Samsara\Exceptions\UsageError\IntegrityConstraint
+ :
+
+ ###### bondYieldToMaturity() Description:
+
+ Calculate the yield to maturity (YTM) of a bond.
+
+ YTM is the internal rate of return on the bond - the discount rate that makes
+ the present value of all future cash flows equal to the current bond price.
+ Uses the internal rate of return method on the bond's cash flows.
+
+ Formula: Find r such that CurrentPrice = Σ(CF_t / (1 + r)^t) where CF includes coupons and face value
+
+---
diff --git a/mkdocs.yml b/mkdocs.yml
index c3728b3c..a56dd743 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -60,6 +60,8 @@ nav:
'Stable Modules':
-
'Coordinate Systems': modules/coordinate-systems/about.md
+ -
+ Finance: modules/finance/about.md
-
Stats: modules/stats/about.md
-
diff --git a/phpunit.xml b/phpunit.xml
index fd4a7413..b583d1c3 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -49,5 +49,8 @@
./tests/Samsara/Fermat/Stats
+
+ ./tests/Samsara/Fermat/Finance
+
diff --git a/src/Samsara/Fermat/Finance/Provider/FinanceProvider.php b/src/Samsara/Fermat/Finance/Provider/FinanceProvider.php
new file mode 100644
index 00000000..6effec4c
--- /dev/null
+++ b/src/Samsara/Fermat/Finance/Provider/FinanceProvider.php
@@ -0,0 +1,761 @@
+multiply($rate)->multiply($time);
+ }
+
+ /**
+ * Calculate compound interest.
+ *
+ * Formula: A = P(1 + r/n)^(nt) for regular compounding
+ * Formula: A = Pe^(rt) for continuous compounding (when n = 0)
+ *
+ * @param int|float|string|Decimal $principal The principal amount
+ * @param int|float|string|Decimal $rate The annual interest rate (as decimal, e.g., 0.05 for 5%)
+ * @param int|float|string|Decimal $time The time period in years
+ * @param int|float|string|Decimal $frequency The number of times interest is compounded per year (use 0 for continuous)
+ *
+ * @return ImmutableDecimal
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function compoundInterest(
+ int|float|string|Decimal $principal,
+ int|float|string|Decimal $rate,
+ int|float|string|Decimal $time,
+ int|float|string|Decimal $frequency = 1
+ ): ImmutableDecimal
+ {
+ /** @var ImmutableDecimal $principal */
+ $principal = Numbers::makeOrDont(Numbers::IMMUTABLE, $principal);
+ /** @var ImmutableDecimal $rate */
+ $rate = Numbers::makeOrDont(Numbers::IMMUTABLE, $rate);
+ /** @var ImmutableDecimal $time */
+ $time = Numbers::makeOrDont(Numbers::IMMUTABLE, $time);
+ /** @var ImmutableDecimal $frequency */
+ $frequency = Numbers::makeOrDont(Numbers::IMMUTABLE, $frequency);
+
+ // Continuous compounding: A = Pe^(rt)
+ if ($frequency->isEqual(0)) {
+ /** @var ImmutableDecimal $exponent */
+ $exponent = $rate->multiply($time);
+ return $principal->multiply($exponent->exp());
+ }
+
+ // Regular compounding: A = P(1 + r/n)^(nt)
+ /** @var ImmutableDecimal $one */
+ $one = Numbers::makeOrDont(Numbers::IMMUTABLE, 1);
+ /** @var ImmutableDecimal $ratePerPeriod */
+ $ratePerPeriod = $rate->divide($frequency);
+ /** @var ImmutableDecimal $base */
+ $base = $one->add($ratePerPeriod);
+ /** @var ImmutableDecimal $exponent */
+ $exponent = $frequency->multiply($time);
+
+ return $principal->multiply($base->pow($exponent));
+ }
+
+ /**
+ * Calculate present value.
+ *
+ * Formula: PV = FV / (1 + r)^n
+ *
+ * @param int|float|string|Decimal $futureValue The future value
+ * @param int|float|string|Decimal $rate The discount rate per period (as decimal, e.g., 0.05 for 5%)
+ * @param int|float|string|Decimal $periods The number of periods
+ *
+ * @return ImmutableDecimal
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function presentValue(
+ int|float|string|Decimal $futureValue,
+ int|float|string|Decimal $rate,
+ int|float|string|Decimal $periods
+ ): ImmutableDecimal
+ {
+ /** @var ImmutableDecimal $futureValue */
+ $futureValue = Numbers::makeOrDont(Numbers::IMMUTABLE, $futureValue);
+ /** @var ImmutableDecimal $rate */
+ $rate = Numbers::makeOrDont(Numbers::IMMUTABLE, $rate);
+ /** @var ImmutableDecimal $periods */
+ $periods = Numbers::makeOrDont(Numbers::IMMUTABLE, $periods);
+
+ // Validate that rate is not -100% or less (would cause division by zero or negative base)
+ /** @var ImmutableDecimal $negativeOne */
+ $negativeOne = Numbers::makeOrDont(Numbers::IMMUTABLE, -1);
+ if ($rate->isLessThanOrEqualTo($negativeOne)) {
+ throw new IntegrityConstraint(
+ 'Rate cannot be -100% or less',
+ 'Provide a rate greater than -1',
+ 'Rate must be > -1 to avoid division by zero or negative base in (1 + r)'
+ );
+ }
+
+ // PV = FV / (1 + r)^n
+ /** @var ImmutableDecimal $one */
+ $one = Numbers::makeOrDont(Numbers::IMMUTABLE, 1);
+ /** @var ImmutableDecimal $base */
+ $base = $one->add($rate);
+ /** @var ImmutableDecimal $divisor */
+ $divisor = $base->pow($periods);
+
+ return $futureValue->divide($divisor);
+ }
+
+ /**
+ * Calculate future value.
+ *
+ * Formula: FV = PV * (1 + r)^n
+ *
+ * @param int|float|string|Decimal $presentValue The present value
+ * @param int|float|string|Decimal $rate The interest rate per period (as decimal, e.g., 0.05 for 5%)
+ * @param int|float|string|Decimal $periods The number of periods
+ *
+ * @return ImmutableDecimal
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function futureValue(
+ int|float|string|Decimal $presentValue,
+ int|float|string|Decimal $rate,
+ int|float|string|Decimal $periods
+ ): ImmutableDecimal
+ {
+ /** @var ImmutableDecimal $presentValue */
+ $presentValue = Numbers::makeOrDont(Numbers::IMMUTABLE, $presentValue);
+ /** @var ImmutableDecimal $rate */
+ $rate = Numbers::makeOrDont(Numbers::IMMUTABLE, $rate);
+ /** @var ImmutableDecimal $periods */
+ $periods = Numbers::makeOrDont(Numbers::IMMUTABLE, $periods);
+
+ // FV = PV * (1 + r)^n
+ /** @var ImmutableDecimal $one */
+ $one = Numbers::makeOrDont(Numbers::IMMUTABLE, 1);
+ /** @var ImmutableDecimal $base */
+ $base = $one->add($rate);
+ /** @var ImmutableDecimal $multiplier */
+ $multiplier = $base->pow($periods);
+
+ return $presentValue->multiply($multiplier);
+ }
+
+ /**
+ * Calculate the present value of an annuity (ordinary annuity).
+ *
+ * Formula: PV = PMT * [(1 - (1 + r)^-n) / r] for r != 0
+ * Formula: PV = PMT * n for r = 0
+ *
+ * @param int|float|string|Decimal $payment The payment amount per period
+ * @param int|float|string|Decimal $rate The interest rate per period (as decimal, e.g., 0.05 for 5%)
+ * @param int|float|string|Decimal $periods The number of periods
+ *
+ * @return ImmutableDecimal
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function annuityPresentValue(
+ int|float|string|Decimal $payment,
+ int|float|string|Decimal $rate,
+ int|float|string|Decimal $periods
+ ): ImmutableDecimal
+ {
+ /** @var ImmutableDecimal $payment */
+ $payment = Numbers::makeOrDont(Numbers::IMMUTABLE, $payment);
+ /** @var ImmutableDecimal $rate */
+ $rate = Numbers::makeOrDont(Numbers::IMMUTABLE, $rate);
+ /** @var ImmutableDecimal $periods */
+ $periods = Numbers::makeOrDont(Numbers::IMMUTABLE, $periods);
+
+ // Edge case: when rate is 0, PV = PMT * n
+ if ($rate->isEqual(0)) {
+ return $payment->multiply($periods);
+ }
+
+ // Validate that rate is not -100% or less (would cause division by zero or negative base)
+ /** @var ImmutableDecimal $negativeOne */
+ $negativeOne = Numbers::makeOrDont(Numbers::IMMUTABLE, -1);
+ if ($rate->isLessThanOrEqualTo($negativeOne)) {
+ throw new IntegrityConstraint(
+ 'Rate cannot be -100% or less',
+ 'Provide a rate greater than -1',
+ 'Rate must be > -1 to avoid division by zero or negative base in (1 + r)'
+ );
+ }
+
+ // PV = PMT * [(1 - (1 + r)^-n) / r]
+ /** @var ImmutableDecimal $one */
+ $one = Numbers::makeOrDont(Numbers::IMMUTABLE, 1);
+ /** @var ImmutableDecimal $base */
+ $base = $one->add($rate);
+ /** @var ImmutableDecimal $negativeN */
+ $negativeN = $periods->multiply($negativeOne);
+ /** @var ImmutableDecimal $discountFactor */
+ $discountFactor = $base->pow($negativeN);
+ /** @var ImmutableDecimal $numerator */
+ $numerator = $one->subtract($discountFactor);
+ /** @var ImmutableDecimal $annuityFactor */
+ $annuityFactor = $numerator->divide($rate);
+
+ return $payment->multiply($annuityFactor);
+ }
+
+ /**
+ * Calculate the future value of an annuity (ordinary annuity).
+ *
+ * Formula: FV = PMT * [((1 + r)^n - 1) / r] for r != 0
+ * Formula: FV = PMT * n for r = 0
+ *
+ * @param int|float|string|Decimal $payment The payment amount per period
+ * @param int|float|string|Decimal $rate The interest rate per period (as decimal, e.g., 0.05 for 5%)
+ * @param int|float|string|Decimal $periods The number of periods
+ *
+ * @return ImmutableDecimal
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function annuityFutureValue(
+ int|float|string|Decimal $payment,
+ int|float|string|Decimal $rate,
+ int|float|string|Decimal $periods
+ ): ImmutableDecimal
+ {
+ /** @var ImmutableDecimal $payment */
+ $payment = Numbers::makeOrDont(Numbers::IMMUTABLE, $payment);
+ /** @var ImmutableDecimal $rate */
+ $rate = Numbers::makeOrDont(Numbers::IMMUTABLE, $rate);
+ /** @var ImmutableDecimal $periods */
+ $periods = Numbers::makeOrDont(Numbers::IMMUTABLE, $periods);
+
+ // Edge case: when rate is 0, FV = PMT * n
+ if ($rate->isEqual(0)) {
+ return $payment->multiply($periods);
+ }
+
+ // Validate that rate is not -100% or less (would cause division by zero or negative base)
+ /** @var ImmutableDecimal $negativeOne */
+ $negativeOne = Numbers::makeOrDont(Numbers::IMMUTABLE, -1);
+ if ($rate->isLessThanOrEqualTo($negativeOne)) {
+ throw new IntegrityConstraint(
+ 'Rate cannot be -100% or less',
+ 'Provide a rate greater than -1',
+ 'Rate must be > -1 to avoid division by zero or negative base in (1 + r)'
+ );
+ }
+
+ // FV = PMT * [((1 + r)^n - 1) / r]
+ /** @var ImmutableDecimal $one */
+ $one = Numbers::makeOrDont(Numbers::IMMUTABLE, 1);
+ /** @var ImmutableDecimal $base */
+ $base = $one->add($rate);
+ /** @var ImmutableDecimal $growthFactor */
+ $growthFactor = $base->pow($periods);
+ /** @var ImmutableDecimal $numerator */
+ $numerator = $growthFactor->subtract($one);
+ /** @var ImmutableDecimal $annuityFactor */
+ $annuityFactor = $numerator->divide($rate);
+
+ return $payment->multiply($annuityFactor);
+ }
+
+ /**
+ * Calculate loan payment amount (ordinary annuity payment).
+ *
+ * Formula: PMT = P * [r(1 + r)^n] / [(1 + r)^n - 1] for r != 0
+ * Formula: PMT = P / n for r = 0
+ *
+ * @param int|float|string|Decimal $principal The principal (loan amount)
+ * @param int|float|string|Decimal $rate The interest rate per period (as decimal, e.g., 0.05 for 5%)
+ * @param int|float|string|Decimal $periods The number of periods
+ *
+ * @return ImmutableDecimal
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function loanPayment(
+ int|float|string|Decimal $principal,
+ int|float|string|Decimal $rate,
+ int|float|string|Decimal $periods
+ ): ImmutableDecimal
+ {
+ /** @var ImmutableDecimal $principal */
+ $principal = Numbers::makeOrDont(Numbers::IMMUTABLE, $principal);
+ /** @var ImmutableDecimal $rate */
+ $rate = Numbers::makeOrDont(Numbers::IMMUTABLE, $rate);
+ /** @var ImmutableDecimal $periods */
+ $periods = Numbers::makeOrDont(Numbers::IMMUTABLE, $periods);
+
+ // Edge case: when rate is 0, PMT = P / n
+ if ($rate->isEqual(0)) {
+ return $principal->divide($periods);
+ }
+
+ // Validate that rate is not -100% or less (would cause division by zero or negative base)
+ /** @var ImmutableDecimal $negativeOne */
+ $negativeOne = Numbers::makeOrDont(Numbers::IMMUTABLE, -1);
+ if ($rate->isLessThanOrEqualTo($negativeOne)) {
+ throw new IntegrityConstraint(
+ 'Rate cannot be -100% or less',
+ 'Provide a rate greater than -1',
+ 'Rate must be > -1 to avoid division by zero or negative base in (1 + r)'
+ );
+ }
+
+ // PMT = P * [r(1 + r)^n] / [(1 + r)^n - 1]
+ /** @var ImmutableDecimal $one */
+ $one = Numbers::makeOrDont(Numbers::IMMUTABLE, 1);
+ /** @var ImmutableDecimal $base */
+ $base = $one->add($rate);
+ /** @var ImmutableDecimal $growthFactor */
+ $growthFactor = $base->pow($periods);
+ /** @var ImmutableDecimal $numerator */
+ $numerator = $rate->multiply($growthFactor);
+ /** @var ImmutableDecimal $denominator */
+ $denominator = $growthFactor->subtract($one);
+ /** @var ImmutableDecimal $paymentFactor */
+ $paymentFactor = $numerator->divide($denominator);
+
+ return $principal->multiply($paymentFactor);
+ }
+
+ /**
+ * Generate a loan amortization schedule.
+ *
+ * Returns an array of payment details for each period, including:
+ * - period: The payment period number
+ * - payment: The total payment amount
+ * - principal: The principal portion of the payment
+ * - interest: The interest portion of the payment
+ * - balance: The remaining loan balance after the payment
+ *
+ * @param int|float|string|Decimal $principal The principal (loan amount)
+ * @param int|float|string|Decimal $rate The interest rate per period (as decimal, e.g., 0.05 for 5%)
+ * @param int|float|string|Decimal $periods The number of periods
+ *
+ * @return array Array of payment details for each period
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function amortizationSchedule(
+ int|float|string|Decimal $principal,
+ int|float|string|Decimal $rate,
+ int|float|string|Decimal $periods
+ ): array
+ {
+ /** @var ImmutableDecimal $principal */
+ $principal = Numbers::makeOrDont(Numbers::IMMUTABLE, $principal);
+ /** @var ImmutableDecimal $rate */
+ $rate = Numbers::makeOrDont(Numbers::IMMUTABLE, $rate);
+ /** @var ImmutableDecimal $periods */
+ $periods = Numbers::makeOrDont(Numbers::IMMUTABLE, $periods);
+
+ // Calculate the periodic payment
+ /** @var ImmutableDecimal $payment */
+ $payment = self::loanPayment($principal, $rate, $periods);
+
+ /** @var ImmutableDecimal $balance */
+ $balance = $principal;
+ /** @var ImmutableDecimal $zero */
+ $zero = Numbers::makeOrDont(Numbers::IMMUTABLE, 0);
+
+ $schedule = [];
+ $periodCount = (int)$periods->getValue();
+
+ for ($i = 1; $i <= $periodCount; $i++) {
+ // Calculate interest for this period
+ /** @var ImmutableDecimal $interest */
+ $interest = $balance->multiply($rate);
+
+ // Calculate principal payment
+ /** @var ImmutableDecimal $principalPayment */
+ $principalPayment = $payment->subtract($interest);
+
+ // Update balance
+ /** @var ImmutableDecimal $newBalance */
+ $newBalance = $balance->subtract($principalPayment);
+
+ // Handle final period rounding - ensure balance is exactly zero
+ if ($i === $periodCount) {
+ $newBalance = $zero;
+ }
+
+ $schedule[] = [
+ 'period' => $i,
+ 'payment' => $payment,
+ 'principal' => $principalPayment,
+ 'interest' => $interest,
+ 'balance' => $newBalance,
+ ];
+
+ $balance = $newBalance;
+ }
+
+ return $schedule;
+ }
+
+ /**
+ * Calculate net present value (NPV) of a series of cash flows.
+ *
+ * Formula: NPV = Σ(CF_t / (1 + r)^t) for t from 0 to n
+ *
+ * @param array $cashFlows Array of cash flows indexed by period (period 0 is initial investment, typically negative)
+ * @param int|float|string|Decimal $rate The discount rate per period (as decimal, e.g., 0.05 for 5%)
+ *
+ * @return ImmutableDecimal
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function netPresentValue(
+ array $cashFlows,
+ int|float|string|Decimal $rate
+ ): ImmutableDecimal
+ {
+ /** @var ImmutableDecimal $rate */
+ $rate = Numbers::makeOrDont(Numbers::IMMUTABLE, $rate);
+
+ // Validate that rate is not -100% or less (would cause division by zero or negative base)
+ /** @var ImmutableDecimal $negativeOne */
+ $negativeOne = Numbers::makeOrDont(Numbers::IMMUTABLE, -1);
+ if ($rate->isLessThanOrEqualTo($negativeOne)) {
+ throw new IntegrityConstraint(
+ 'Rate cannot be -100% or less',
+ 'Provide a rate greater than -1',
+ 'Rate must be > -1 to avoid division by zero or negative base in (1 + r)'
+ );
+ }
+
+ /** @var ImmutableDecimal $npv */
+ $npv = Numbers::makeOrDont(Numbers::IMMUTABLE, 0);
+ /** @var ImmutableDecimal $one */
+ $one = Numbers::makeOrDont(Numbers::IMMUTABLE, 1);
+ /** @var ImmutableDecimal $base */
+ $base = $one->add($rate);
+
+ // Calculate NPV by summing the present value of each cash flow
+ foreach ($cashFlows as $period => $cashFlow) {
+ /** @var ImmutableDecimal $cashFlow */
+ $cashFlow = Numbers::makeOrDont(Numbers::IMMUTABLE, $cashFlow);
+ /** @var ImmutableDecimal $periodNum */
+ $periodNum = Numbers::makeOrDont(Numbers::IMMUTABLE, $period);
+
+ // Calculate present value of this cash flow: CF / (1 + r)^t
+ /** @var ImmutableDecimal $discountFactor */
+ $discountFactor = $base->pow($periodNum);
+ /** @var ImmutableDecimal $presentValue */
+ $presentValue = $cashFlow->divide($discountFactor);
+
+ // Add to running total
+ $npv = $npv->add($presentValue);
+ }
+
+ return $npv;
+ }
+
+ /**
+ * Calculate the internal rate of return (IRR) for a series of cash flows.
+ *
+ * IRR is the discount rate that makes the net present value (NPV) equal to zero.
+ * Uses the Newton-Raphson method for iterative approximation.
+ *
+ * Formula: Find r such that NPV = Σ(CF_t / (1 + r)^t) = 0
+ *
+ * @param array $cashFlows Array of cash flows indexed by period (period 0 is initial investment, typically negative)
+ * @param int|float|string|Decimal $guess Initial guess for IRR (default 0.1 for 10%)
+ * @param int|float|string|Decimal $tolerance Convergence tolerance (default 0.000001)
+ * @param int $maxIterations Maximum number of iterations (default 100)
+ *
+ * @return ImmutableDecimal
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function internalRateOfReturn(
+ array $cashFlows,
+ int|float|string|Decimal $guess = 0.1,
+ int|float|string|Decimal $tolerance = 0.000001,
+ int $maxIterations = 100
+ ): ImmutableDecimal
+ {
+ // Validate cash flows
+ if (empty($cashFlows)) {
+ throw new IntegrityConstraint(
+ 'Cash flows array cannot be empty',
+ 'Provide at least one cash flow',
+ 'IRR calculation requires at least one cash flow'
+ );
+ }
+
+ /** @var ImmutableDecimal $rate */
+ $rate = Numbers::makeOrDont(Numbers::IMMUTABLE, $guess);
+ /** @var ImmutableDecimal $tolerance */
+ $tolerance = Numbers::makeOrDont(Numbers::IMMUTABLE, $tolerance);
+ /** @var ImmutableDecimal $one */
+ $one = Numbers::makeOrDont(Numbers::IMMUTABLE, 1);
+ /** @var ImmutableDecimal $negativeOne */
+ $negativeOne = Numbers::makeOrDont(Numbers::IMMUTABLE, -1);
+
+ // Newton-Raphson iteration
+ for ($i = 0; $i < $maxIterations; $i++) {
+ // Calculate NPV at current rate
+ /** @var ImmutableDecimal $npv */
+ $npv = Numbers::makeOrDont(Numbers::IMMUTABLE, 0);
+ /** @var ImmutableDecimal $npvDerivative */
+ $npvDerivative = Numbers::makeOrDont(Numbers::IMMUTABLE, 0);
+ /** @var ImmutableDecimal $base */
+ $base = $one->add($rate);
+
+ // Calculate NPV and its derivative
+ foreach ($cashFlows as $period => $cashFlow) {
+ /** @var ImmutableDecimal $cashFlow */
+ $cashFlow = Numbers::makeOrDont(Numbers::IMMUTABLE, $cashFlow);
+ /** @var ImmutableDecimal $periodNum */
+ $periodNum = Numbers::makeOrDont(Numbers::IMMUTABLE, $period);
+
+ // NPV calculation: CF / (1 + r)^t
+ /** @var ImmutableDecimal $discountFactor */
+ $discountFactor = $base->pow($periodNum);
+ /** @var ImmutableDecimal $presentValue */
+ $presentValue = $cashFlow->divide($discountFactor);
+ $npv = $npv->add($presentValue);
+
+ // Derivative calculation: -t * CF / (1 + r)^(t+1)
+ if ($periodNum->isGreaterThan(0)) {
+ /** @var ImmutableDecimal $derivativeTerm */
+ $derivativeTerm = $periodNum->multiply($negativeOne)->multiply($cashFlow);
+ /** @var ImmutableDecimal $derivativeDivisor */
+ $derivativeDivisor = $base->pow($periodNum->add($one));
+ /** @var ImmutableDecimal $derivativeValue */
+ $derivativeValue = $derivativeTerm->divide($derivativeDivisor);
+ $npvDerivative = $npvDerivative->add($derivativeValue);
+ }
+ }
+
+ // Check for convergence
+ if ($npv->abs()->isLessThan($tolerance)) {
+ return $rate;
+ }
+
+ // Check for zero derivative (would cause division by zero)
+ /** @var ImmutableDecimal $zero */
+ $zero = Numbers::makeOrDont(Numbers::IMMUTABLE, 0);
+ if ($npvDerivative->isEqual($zero)) {
+ throw new IntegrityConstraint(
+ 'IRR calculation failed: derivative is zero',
+ 'Try a different initial guess',
+ 'The Newton-Raphson method encountered a zero derivative'
+ );
+ }
+
+ // Newton-Raphson update: r_new = r_old - NPV / NPV'
+ /** @var ImmutableDecimal $delta */
+ $delta = $npv->divide($npvDerivative);
+ $rate = $rate->subtract($delta);
+
+ // Validate that rate stays above -100%
+ if ($rate->isLessThanOrEqualTo($negativeOne)) {
+ throw new IntegrityConstraint(
+ 'IRR calculation failed: rate converged to -100% or less',
+ 'The cash flows may not have a valid IRR',
+ 'IRR must be greater than -100%'
+ );
+ }
+ }
+
+ // Maximum iterations reached
+ throw new IntegrityConstraint(
+ 'IRR calculation did not converge within maximum iterations',
+ 'Try increasing maxIterations or adjusting the initial guess',
+ 'The Newton-Raphson method did not converge within ' . $maxIterations . ' iterations'
+ );
+ }
+
+ /**
+ * Calculate the price of a bond.
+ *
+ * Formula: P = C * [(1 - (1 + r)^-n) / r] + F / (1 + r)^n for r != 0
+ * Formula: P = C * n + F for r = 0
+ *
+ * @param int|float|string|Decimal $faceValue The face value (par value) of the bond
+ * @param int|float|string|Decimal $couponRate The coupon rate per period (as decimal, e.g., 0.05 for 5%)
+ * @param int|float|string|Decimal $marketRate The market interest rate (yield) per period (as decimal, e.g., 0.04 for 4%)
+ * @param int|float|string|Decimal $periods The number of periods until maturity
+ *
+ * @return ImmutableDecimal
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function bondPrice(
+ int|float|string|Decimal $faceValue,
+ int|float|string|Decimal $couponRate,
+ int|float|string|Decimal $marketRate,
+ int|float|string|Decimal $periods
+ ): ImmutableDecimal
+ {
+ /** @var ImmutableDecimal $faceValue */
+ $faceValue = Numbers::makeOrDont(Numbers::IMMUTABLE, $faceValue);
+ /** @var ImmutableDecimal $couponRate */
+ $couponRate = Numbers::makeOrDont(Numbers::IMMUTABLE, $couponRate);
+ /** @var ImmutableDecimal $marketRate */
+ $marketRate = Numbers::makeOrDont(Numbers::IMMUTABLE, $marketRate);
+ /** @var ImmutableDecimal $periods */
+ $periods = Numbers::makeOrDont(Numbers::IMMUTABLE, $periods);
+
+ // Calculate coupon payment: C = Face Value * Coupon Rate
+ /** @var ImmutableDecimal $couponPayment */
+ $couponPayment = $faceValue->multiply($couponRate);
+
+ // Edge case: when market rate is 0, P = C * n + F
+ if ($marketRate->isEqual(0)) {
+ /** @var ImmutableDecimal $couponsPV */
+ $couponsPV = $couponPayment->multiply($periods);
+ return $couponsPV->add($faceValue);
+ }
+
+ // Validate that market rate is not -100% or less (would cause division by zero or negative base)
+ /** @var ImmutableDecimal $negativeOne */
+ $negativeOne = Numbers::makeOrDont(Numbers::IMMUTABLE, -1);
+ if ($marketRate->isLessThanOrEqualTo($negativeOne)) {
+ throw new IntegrityConstraint(
+ 'Market rate cannot be -100% or less',
+ 'Provide a market rate greater than -1',
+ 'Market rate must be > -1 to avoid division by zero or negative base in (1 + r)'
+ );
+ }
+
+ // Calculate present value of coupon payments (annuity)
+ // PV of coupons = C * [(1 - (1 + r)^-n) / r]
+ /** @var ImmutableDecimal $couponsPV */
+ $couponsPV = self::annuityPresentValue($couponPayment, $marketRate, $periods);
+
+ // Calculate present value of face value
+ // PV of face value = F / (1 + r)^n
+ /** @var ImmutableDecimal $faceValuePV */
+ $faceValuePV = self::presentValue($faceValue, $marketRate, $periods);
+
+ // Bond price = PV of coupons + PV of face value
+ return $couponsPV->add($faceValuePV);
+ }
+
+ /**
+ * Calculate the yield to maturity (YTM) of a bond.
+ *
+ * YTM is the internal rate of return on the bond - the discount rate that makes
+ * the present value of all future cash flows equal to the current bond price.
+ * Uses the internal rate of return method on the bond's cash flows.
+ *
+ * Formula: Find r such that CurrentPrice = Σ(CF_t / (1 + r)^t) where CF includes coupons and face value
+ *
+ * @param int|float|string|Decimal $currentPrice The current market price of the bond
+ * @param int|float|string|Decimal $faceValue The face value (par value) of the bond
+ * @param int|float|string|Decimal $couponRate The coupon rate per period (as decimal, e.g., 0.05 for 5%)
+ * @param int|float|string|Decimal $periods The number of periods until maturity
+ * @param int|float|string|Decimal $guess Initial guess for YTM (default 0.05 for 5%)
+ * @param int|float|string|Decimal $tolerance Convergence tolerance (default 0.000001)
+ * @param int $maxIterations Maximum number of iterations (default 100)
+ *
+ * @return ImmutableDecimal
+ * @throws IncompatibleObjectState
+ * @throws IntegrityConstraint
+ */
+ public static function bondYieldToMaturity(
+ int|float|string|Decimal $currentPrice,
+ int|float|string|Decimal $faceValue,
+ int|float|string|Decimal $couponRate,
+ int|float|string|Decimal $periods,
+ int|float|string|Decimal $guess = null,
+ int|float|string|Decimal $tolerance = 0.000001,
+ int $maxIterations = 100
+ ): ImmutableDecimal
+ {
+ /** @var ImmutableDecimal $currentPrice */
+ $currentPrice = Numbers::makeOrDont(Numbers::IMMUTABLE, $currentPrice);
+ /** @var ImmutableDecimal $faceValue */
+ $faceValue = Numbers::makeOrDont(Numbers::IMMUTABLE, $faceValue);
+ /** @var ImmutableDecimal $couponRate */
+ $couponRate = Numbers::makeOrDont(Numbers::IMMUTABLE, $couponRate);
+ /** @var ImmutableDecimal $periods */
+ $periods = Numbers::makeOrDont(Numbers::IMMUTABLE, $periods);
+
+ // Calculate coupon payment: C = Face Value * Coupon Rate
+ /** @var ImmutableDecimal $couponPayment */
+ $couponPayment = $faceValue->multiply($couponRate);
+
+ // If no guess provided, use a financial approximation for better initial guess
+ // YTM ≈ [C + (F - P) / n] / [(F + P) / 2]
+ if ($guess === null) {
+ /** @var ImmutableDecimal $numerator */
+ $numerator = $couponPayment->add($faceValue->subtract($currentPrice)->divide($periods));
+ /** @var ImmutableDecimal $denominator */
+ $denominator = $faceValue->add($currentPrice)->divide(Numbers::makeOrDont(Numbers::IMMUTABLE, 2));
+ /** @var ImmutableDecimal $initialGuess */
+ $initialGuess = $numerator->divide($denominator);
+ } else {
+ $initialGuess = Numbers::makeOrDont(Numbers::IMMUTABLE, $guess);
+ }
+
+ // Build cash flows array for IRR calculation
+ // Period 0: negative of current price (initial investment)
+ // Periods 1 to n-1: coupon payments
+ // Period n: coupon payment + face value
+ $cashFlows = [];
+
+ // Period 0: Initial investment (negative of current price)
+ /** @var ImmutableDecimal $negativePrice */
+ $negativePrice = $currentPrice->multiply(Numbers::makeOrDont(Numbers::IMMUTABLE, -1));
+ $cashFlows[0] = $negativePrice;
+
+ // Periods 1 to n: Build the cash flows
+ $periodCount = (int)$periods->getValue();
+ for ($i = 1; $i <= $periodCount; $i++) {
+ if ($i === $periodCount) {
+ // Last period: coupon payment + face value
+ $cashFlows[$i] = $couponPayment->add($faceValue);
+ } else {
+ // Regular coupon payment
+ $cashFlows[$i] = $couponPayment;
+ }
+ }
+
+ // Use the internal rate of return method to find YTM
+ return self::internalRateOfReturn($cashFlows, $initialGuess, $tolerance, $maxIterations);
+ }
+
+}
diff --git a/tests/Samsara/Fermat/Finance/Provider/FinanceProviderTest.php b/tests/Samsara/Fermat/Finance/Provider/FinanceProviderTest.php
new file mode 100644
index 00000000..961f0bb7
--- /dev/null
+++ b/tests/Samsara/Fermat/Finance/Provider/FinanceProviderTest.php
@@ -0,0 +1,462 @@
+assertEquals('100', $answer1->getValue());
+
+ // Test case 2: Principal = 5000, Rate = 3.5% (0.035), Time = 3 years
+ // Expected: 5000 * 0.035 * 3 = 525
+ $answer2 = FinanceProvider::simpleInterest(5000, 0.035, 3);
+ $this->assertEquals('525', $answer2->getValue());
+
+ // Test case 3: Principal = 10000, Rate = 4.25% (0.0425), Time = 1 year
+ // Expected: 10000 * 0.0425 * 1 = 425
+ $answer3 = FinanceProvider::simpleInterest(10000, 0.0425, 1);
+ $this->assertEquals('425', $answer3->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testCompoundInterest()
+ {
+ // Test case 1: Annual compounding
+ // Principal = 1000, Rate = 5% (0.05), Time = 2 years, Frequency = 1 (annual)
+ // Expected: 1000(1 + 0.05/1)^(1*2) = 1000 * 1.05^2 = 1102.5
+ $answer1 = FinanceProvider::compoundInterest(1000, 0.05, 2, 1);
+ $this->assertEquals('1102.5', $answer1->getValue());
+
+ // Test case 2: Quarterly compounding
+ // Principal = 5000, Rate = 6% (0.06), Time = 3 years, Frequency = 4 (quarterly)
+ // Expected: 5000(1 + 0.06/4)^(4*3) = 5000 * 1.015^12 ≈ 5978.09
+ $answer2 = FinanceProvider::compoundInterest(5000, 0.06, 3, 4);
+ $this->assertEquals('5978.09', $answer2->round(2)->getValue());
+
+ // Test case 3: Monthly compounding
+ // Principal = 10000, Rate = 4% (0.04), Time = 5 years, Frequency = 12 (monthly)
+ // Expected: 10000(1 + 0.04/12)^(12*5) ≈ 12209.97
+ $answer3 = FinanceProvider::compoundInterest(10000, 0.04, 5, 12);
+ $this->assertEquals('12209.97', $answer3->round(2)->getValue());
+
+ // Test case 4: Continuous compounding
+ // Principal = 1000, Rate = 5% (0.05), Time = 2 years, Frequency = 0 (continuous)
+ // Expected: 1000 * e^(0.05*2) = 1000 * e^0.1 ≈ 1105.17
+ $answer4 = FinanceProvider::compoundInterest(1000, 0.05, 2, 0);
+ $this->assertEquals('1105.17', $answer4->round(2)->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testPresentValue()
+ {
+ // Test case 1: Basic present value
+ // Future Value = 1102.5, Rate = 5% (0.05), Periods = 2
+ // Expected: 1102.5 / (1.05)^2 = 1102.5 / 1.1025 = 1000
+ $answer1 = FinanceProvider::presentValue(1102.5, 0.05, 2);
+ $this->assertEquals('1000', $answer1->getValue());
+
+ // Test case 2: Present value with different rate
+ // Future Value = 5000, Rate = 8% (0.08), Periods = 3
+ // Expected: 5000 / (1.08)^3 = 5000 / 1.259712 ≈ 3969.16
+ $answer2 = FinanceProvider::presentValue(5000, 0.08, 3);
+ $this->assertEquals('3969.16', $answer2->round(2)->getValue());
+
+ // Test case 3: Present value with fractional rate
+ // Future Value = 10000, Rate = 4.5% (0.045), Periods = 5
+ // Expected: 10000 / (1.045)^5 = 10000 / 1.24618... ≈ 8024.51
+ $answer3 = FinanceProvider::presentValue(10000, 0.045, 5);
+ $this->assertEquals('8024.51', $answer3->round(2)->getValue());
+
+ // Test case 4: Edge case with zero rate
+ // Future Value = 1000, Rate = 0% (0), Periods = 5
+ // Expected: 1000 / (1.0)^5 = 1000
+ $answer4 = FinanceProvider::presentValue(1000, 0, 5);
+ $this->assertEquals('1000', $answer4->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testFutureValue()
+ {
+ // Test case 1: Basic future value
+ // Present Value = 1000, Rate = 5% (0.05), Periods = 2
+ // Expected: 1000 * (1.05)^2 = 1000 * 1.1025 = 1102.5
+ $answer1 = FinanceProvider::futureValue(1000, 0.05, 2);
+ $this->assertEquals('1102.5', $answer1->getValue());
+
+ // Test case 2: Future value with different rate
+ // Present Value = 3969.16, Rate = 8% (0.08), Periods = 3
+ // Expected: 3969.16 * (1.08)^3 = 3969.16 * 1.259712 ≈ 5000
+ $answer2 = FinanceProvider::futureValue(3969.16, 0.08, 3);
+ $this->assertEquals('5000', $answer2->round(2)->getValue());
+
+ // Test case 3: Future value with fractional rate
+ // Present Value = 8024.51, Rate = 4.5% (0.045), Periods = 5
+ // Expected: 8024.51 * (1.045)^5 = 8024.51 * 1.24618... ≈ 10000
+ $answer3 = FinanceProvider::futureValue(8024.51, 0.045, 5);
+ $this->assertEquals('10000', $answer3->round(2)->getValue());
+
+ // Test case 4: Edge case with zero rate
+ // Present Value = 1000, Rate = 0% (0), Periods = 5
+ // Expected: 1000 * (1.0)^5 = 1000
+ $answer4 = FinanceProvider::futureValue(1000, 0, 5);
+ $this->assertEquals('1000', $answer4->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testAnnuityPresentValue()
+ {
+ // Test case 1: Basic annuity present value
+ // Payment = 100, Rate = 5% (0.05), Periods = 10
+ // Expected: 100 * [(1 - (1.05)^-10) / 0.05] = 100 * 7.72173... ≈ 772.17
+ $answer1 = FinanceProvider::annuityPresentValue(100, 0.05, 10);
+ $this->assertEquals('772.17', $answer1->round(2)->getValue());
+
+ // Test case 2: Annuity with different rate
+ // Payment = 500, Rate = 6% (0.06), Periods = 5
+ // Expected: 500 * [(1 - (1.06)^-5) / 0.06] = 500 * 4.21236... ≈ 2106.18
+ $answer2 = FinanceProvider::annuityPresentValue(500, 0.06, 5);
+ $this->assertEquals('2106.18', $answer2->round(2)->getValue());
+
+ // Test case 3: Longer term annuity
+ // Payment = 1000, Rate = 4% (0.04), Periods = 20
+ // Expected: 1000 * [(1 - (1.04)^-20) / 0.04] = 1000 * 13.59032... ≈ 13590.33
+ $answer3 = FinanceProvider::annuityPresentValue(1000, 0.04, 20);
+ $this->assertEquals('13590.33', $answer3->round(2)->getValue());
+
+ // Test case 4: Edge case with zero rate
+ // Payment = 100, Rate = 0% (0), Periods = 10
+ // Expected: 100 * 10 = 1000
+ $answer4 = FinanceProvider::annuityPresentValue(100, 0, 10);
+ $this->assertEquals('1000', $answer4->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testAnnuityFutureValue()
+ {
+ // Test case 1: Basic annuity future value
+ // Payment = 100, Rate = 5% (0.05), Periods = 10
+ // Expected: 100 * [((1.05)^10 - 1) / 0.05] = 100 * 12.57789... ≈ 1257.79
+ $answer1 = FinanceProvider::annuityFutureValue(100, 0.05, 10);
+ $this->assertEquals('1257.79', $answer1->round(2)->getValue());
+
+ // Test case 2: Annuity with different rate
+ // Payment = 500, Rate = 6% (0.06), Periods = 5
+ // Expected: 500 * [((1.06)^5 - 1) / 0.06] = 500 * 5.63709... ≈ 2818.55
+ $answer2 = FinanceProvider::annuityFutureValue(500, 0.06, 5);
+ $this->assertEquals('2818.55', $answer2->round(2)->getValue());
+
+ // Test case 3: Longer term annuity
+ // Payment = 1000, Rate = 4% (0.04), Periods = 20
+ // Expected: 1000 * [((1.04)^20 - 1) / 0.04] = 1000 * 29.77807... ≈ 29778.08
+ $answer3 = FinanceProvider::annuityFutureValue(1000, 0.04, 20);
+ $this->assertEquals('29778.08', $answer3->round(2)->getValue());
+
+ // Test case 4: Edge case with zero rate
+ // Payment = 100, Rate = 0% (0), Periods = 10
+ // Expected: 100 * 10 = 1000
+ $answer4 = FinanceProvider::annuityFutureValue(100, 0, 10);
+ $this->assertEquals('1000', $answer4->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testLoanPayment()
+ {
+ // Test case 1: Basic loan payment
+ // Principal = 10000, Rate = 5% (0.05), Periods = 12
+ // Expected: 10000 * [0.05(1.05)^12] / [(1.05)^12 - 1] ≈ 1128.25
+ $answer1 = FinanceProvider::loanPayment(10000, 0.05, 12);
+ $this->assertEquals('1128.25', $answer1->round(2)->getValue());
+
+ // Test case 2: Loan with different rate
+ // Principal = 20000, Rate = 6% (0.06), Periods = 24
+ // Expected: 20000 * [0.06(1.06)^24] / [(1.06)^24 - 1] ≈ 1593.58
+ $answer2 = FinanceProvider::loanPayment(20000, 0.06, 24);
+ $this->assertEquals('1593.58', $answer2->round(2)->getValue());
+
+ // Test case 3: Loan with different terms
+ // Principal = 5000, Rate = 3% (0.03), Periods = 10
+ // Expected: 5000 * [0.03(1.03)^10] / [(1.03)^10 - 1] ≈ 586.15
+ $answer3 = FinanceProvider::loanPayment(5000, 0.03, 10);
+ $this->assertEquals('586.15', $answer3->round(2)->getValue());
+
+ // Test case 4: Edge case with zero rate
+ // Principal = 10000, Rate = 0% (0), Periods = 10
+ // Expected: 10000 / 10 = 1000
+ $answer4 = FinanceProvider::loanPayment(10000, 0, 10);
+ $this->assertEquals('1000', $answer4->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testAmortizationSchedule()
+ {
+ // Test case 1: Simple loan amortization
+ // Principal = 10000, Rate = 5% (0.05), Periods = 12
+ // Payment ≈ 1128.25 per period
+ $schedule = FinanceProvider::amortizationSchedule(10000, 0.05, 12);
+
+ // Verify we have 12 periods
+ $this->assertCount(12, $schedule);
+
+ // Verify first payment details
+ // Period 1: Balance = 10000, Interest = 10000 * 0.05 = 500
+ // Principal = 1128.25 - 500 = 628.25, New Balance = 10000 - 628.25 = 9371.75
+ $this->assertEquals(1, $schedule[0]['period']);
+ $this->assertEquals('1128.25', $schedule[0]['payment']->round(2)->getValue());
+ $this->assertEquals('500', $schedule[0]['interest']->round(2)->getValue());
+ $this->assertEquals('628.25', $schedule[0]['principal']->round(2)->getValue());
+ $this->assertEquals('9371.75', $schedule[0]['balance']->round(2)->getValue());
+
+ // Verify last payment
+ $this->assertEquals(12, $schedule[11]['period']);
+ $this->assertEquals('0', $schedule[11]['balance']->getValue());
+
+ // Verify sum of principal payments equals original principal
+ $totalPrincipal = array_reduce($schedule, function($carry, $item) {
+ return $carry->add($item['principal']);
+ }, \Samsara\Fermat\Core\Numbers::makeOrDont(\Samsara\Fermat\Core\Numbers::IMMUTABLE, 0));
+ $this->assertEquals('10000', $totalPrincipal->round(2)->getValue());
+
+ // Test case 2: Loan with zero interest
+ // Principal = 1000, Rate = 0% (0), Periods = 10
+ // Payment = 1000 / 10 = 100 per period
+ $schedule2 = FinanceProvider::amortizationSchedule(1000, 0, 10);
+
+ $this->assertCount(10, $schedule2);
+ $this->assertEquals('100', $schedule2[0]['payment']->getValue());
+ $this->assertEquals('0', $schedule2[0]['interest']->getValue());
+ $this->assertEquals('100', $schedule2[0]['principal']->getValue());
+ $this->assertEquals('900', $schedule2[0]['balance']->getValue());
+ $this->assertEquals('0', $schedule2[9]['balance']->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testNetPresentValue()
+ {
+ // Test case 1: Basic NPV calculation
+ // Cash flows: [-1000, 300, 300, 300, 300], Rate = 10% (0.1)
+ // Expected: -1000 + 300/1.1 + 300/1.1^2 + 300/1.1^3 + 300/1.1^4
+ // = -1000 + 272.727272 + 247.933884 + 225.394440 + 204.904036 ≈ -49.04
+ $cashFlows1 = [0 => -1000, 1 => 300, 2 => 300, 3 => 300, 4 => 300];
+ $answer1 = FinanceProvider::netPresentValue($cashFlows1, 0.1);
+ $this->assertEquals('-49.04', $answer1->round(2)->getValue());
+
+ // Test case 2: Positive NPV (profitable project)
+ // Cash flows: [-1000, 400, 400, 400, 400], Rate = 10% (0.1)
+ // Expected: -1000 + 400/1.1 + 400/1.1^2 + 400/1.1^3 + 400/1.1^4
+ // = -1000 + 363.636363 + 330.578512 + 300.526102 + 273.205547 ≈ 267.95
+ $cashFlows2 = [0 => -1000, 1 => 400, 2 => 400, 3 => 400, 4 => 400];
+ $answer2 = FinanceProvider::netPresentValue($cashFlows2, 0.1);
+ $this->assertEquals('267.95', $answer2->round(2)->getValue());
+
+ // Test case 3: NPV with varying cash flows
+ // Cash flows: [-5000, 1000, 2000, 3000, 2000], Rate = 8% (0.08)
+ // Expected: -5000 + 1000/1.08 + 2000/1.08^2 + 3000/1.08^3 + 2000/1.08^4
+ // = -5000 + 925.92592 + 1714.67764 + 2381.49672 + 1470.05971 ≈ 1492.16
+ $cashFlows3 = [0 => -5000, 1 => 1000, 2 => 2000, 3 => 3000, 4 => 2000];
+ $answer3 = FinanceProvider::netPresentValue($cashFlows3, 0.08);
+ $this->assertEquals('1492.16', $answer3->round(2)->getValue());
+
+ // Test case 4: Edge case with zero rate
+ // Cash flows: [-1000, 300, 300, 300, 300], Rate = 0% (0)
+ // Expected: -1000 + 300 + 300 + 300 + 300 = 200
+ $cashFlows4 = [0 => -1000, 1 => 300, 2 => 300, 3 => 300, 4 => 300];
+ $answer4 = FinanceProvider::netPresentValue($cashFlows4, 0);
+ $this->assertEquals('200', $answer4->getValue());
+
+ // Test case 5: Single period NPV
+ // Cash flows: [-1000, 1100], Rate = 10% (0.1)
+ // Expected: -1000 + 1100/1.1 = -1000 + 1000 = 0
+ $cashFlows5 = [0 => -1000, 1 => 1100];
+ $answer5 = FinanceProvider::netPresentValue($cashFlows5, 0.1);
+ $this->assertEquals('0', $answer5->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testInternalRateOfReturn()
+ {
+ // Test case 1: Basic IRR calculation
+ // Cash flows: [-1000, 1100], should have IRR = 10% (0.1)
+ // Verification: -1000 + 1100/(1.1)^1 = -1000 + 1000 = 0
+ $cashFlows1 = [0 => -1000, 1 => 1100];
+ $answer1 = FinanceProvider::internalRateOfReturn($cashFlows1);
+ $this->assertEquals('0.1', $answer1->round(4)->getValue());
+
+ // Test case 2: Multi-period IRR
+ // Cash flows: [-1000, 300, 400, 500, 200]
+ // IRR should be approximately 11.79%
+ $cashFlows2 = [0 => -1000, 1 => 300, 2 => 400, 3 => 500, 4 => 200];
+ $answer2 = FinanceProvider::internalRateOfReturn($cashFlows2);
+ // Verify that NPV at this rate is approximately zero
+ $npv2 = FinanceProvider::netPresentValue($cashFlows2, $answer2);
+ $this->assertEquals('0', $npv2->round(2)->getValue());
+
+ // Test case 3: Standard project cash flows
+ // Cash flows: [-5000, 1000, 2000, 2000, 3000]
+ // This represents a project with initial investment and increasing returns
+ $cashFlows3 = [0 => -5000, 1 => 1000, 2 => 2000, 3 => 2000, 4 => 3000];
+ $answer3 = FinanceProvider::internalRateOfReturn($cashFlows3);
+ // Verify that NPV at this rate is approximately zero
+ $npv3 = FinanceProvider::netPresentValue($cashFlows3, $answer3);
+ $this->assertEquals('0', $npv3->round(2)->getValue());
+
+ // Test case 4: IRR with different initial guess
+ // Cash flows: [-1000, 400, 400, 400, 400]
+ // Using a different initial guess should still converge
+ $cashFlows4 = [0 => -1000, 1 => 400, 2 => 400, 3 => 400, 4 => 400];
+ $answer4 = FinanceProvider::internalRateOfReturn($cashFlows4, 0.2);
+ // Verify that NPV at this rate is approximately zero
+ $npv4 = FinanceProvider::netPresentValue($cashFlows4, $answer4);
+ $this->assertEquals('0', $npv4->round(2)->getValue());
+
+ // Test case 5: Simple two-period case with known IRR
+ // Cash flows: [-2000, 2200], IRR = 10% (0.1)
+ // Verification: -2000 + 2200/(1.1)^1 = -2000 + 2000 = 0
+ $cashFlows5 = [0 => -2000, 1 => 2200];
+ $answer5 = FinanceProvider::internalRateOfReturn($cashFlows5);
+ $this->assertEquals('0.1', $answer5->round(4)->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testBondPrice()
+ {
+ // Test case 1: Bond at par (coupon rate = market rate)
+ // Face Value = 1000, Coupon Rate = 5% (0.05), Market Rate = 5% (0.05), Periods = 10
+ // Expected: Bond should be priced at par value = 1000
+ // Coupon Payment = 1000 * 0.05 = 50
+ // Price = 50 * [(1 - 1.05^-10) / 0.05] + 1000 / 1.05^10 = 386.09 + 613.91 = 1000
+ $answer1 = FinanceProvider::bondPrice(1000, 0.05, 0.05, 10);
+ $this->assertEquals('1000', $answer1->round(2)->getValue());
+
+ // Test case 2: Bond at premium (coupon rate > market rate)
+ // Face Value = 1000, Coupon Rate = 6% (0.06), Market Rate = 5% (0.05), Periods = 10
+ // Expected: Bond should be priced above par
+ // Coupon Payment = 1000 * 0.06 = 60
+ // Price = 60 * [(1 - 1.05^-10) / 0.05] + 1000 / 1.05^10
+ // = 60 * 7.72173... + 613.91... = 463.30 + 613.91 = 1077.21
+ $answer2 = FinanceProvider::bondPrice(1000, 0.06, 0.05, 10);
+ $this->assertEquals('1077.22', $answer2->round(2)->getValue());
+
+ // Test case 3: Bond at discount (coupon rate < market rate)
+ // Face Value = 1000, Coupon Rate = 4% (0.04), Market Rate = 5% (0.05), Periods = 10
+ // Expected: Bond should be priced below par
+ // Coupon Payment = 1000 * 0.04 = 40
+ // Price = 40 * [(1 - 1.05^-10) / 0.05] + 1000 / 1.05^10
+ // = 40 * 7.72173... + 613.91... = 308.87 + 613.91 = 922.78
+ $answer3 = FinanceProvider::bondPrice(1000, 0.04, 0.05, 10);
+ $this->assertEquals('922.78', $answer3->round(2)->getValue());
+
+ // Test case 4: Zero-coupon bond
+ // Face Value = 1000, Coupon Rate = 0% (0), Market Rate = 5% (0.05), Periods = 10
+ // Expected: Price = Face Value / (1 + r)^n = 1000 / 1.05^10 = 613.91
+ $answer4 = FinanceProvider::bondPrice(1000, 0, 0.05, 10);
+ $this->assertEquals('613.91', $answer4->round(2)->getValue());
+
+ // Test case 5: Bond with different parameters
+ // Face Value = 5000, Coupon Rate = 7% (0.07), Market Rate = 6% (0.06), Periods = 20
+ // Coupon Payment = 5000 * 0.07 = 350
+ // Price = 350 * [(1 - 1.06^-20) / 0.06] + 5000 / 1.06^20
+ // = 350 * 11.46993... + 1559.02... = 4014.48 + 1559.02 = 5573.50
+ $answer5 = FinanceProvider::bondPrice(5000, 0.07, 0.06, 20);
+ $this->assertEquals('5573.5', $answer5->round(2)->getValue());
+
+ // Test case 6: Edge case with zero market rate
+ // Face Value = 1000, Coupon Rate = 5% (0.05), Market Rate = 0% (0), Periods = 10
+ // Expected: Price = (50 * 10) + 1000 = 500 + 1000 = 1500
+ $answer6 = FinanceProvider::bondPrice(1000, 0.05, 0, 10);
+ $this->assertEquals('1500', $answer6->getValue());
+ }
+
+ /**
+ * @small
+ */
+ public function testBondYieldToMaturity()
+ {
+ // Test case 1: Bond at par (price = face value, YTM should equal coupon rate)
+ // Current Price = 1000, Face Value = 1000, Coupon Rate = 5% (0.05), Periods = 10
+ // Expected: YTM = 5% (0.05)
+ $answer1 = FinanceProvider::bondYieldToMaturity(1000, 1000, 0.05, 10);
+ $this->assertEquals('0.05', $answer1->round(4)->getValue());
+
+ // Test case 2: Bond at premium (price > face value, YTM should be less than coupon rate)
+ // Using the bond price from testBondPrice: Price = 1077.22, Face = 1000, Coupon = 6%, Periods = 10
+ // Expected: YTM should be approximately 5% (0.05)
+ $answer2 = FinanceProvider::bondYieldToMaturity(1077.22, 1000, 0.06, 10);
+ $this->assertEquals('0.05', $answer2->round(4)->getValue());
+ // Verify by calculating bond price with this YTM
+ $verifyPrice2 = FinanceProvider::bondPrice(1000, 0.06, $answer2, 10);
+ $this->assertEquals('1077.22', $verifyPrice2->round(2)->getValue());
+
+ // Test case 3: Bond at discount (price < face value, YTM should be greater than coupon rate)
+ // Using the bond price from testBondPrice: Price = 922.78, Face = 1000, Coupon = 4%, Periods = 10
+ // Expected: YTM should be approximately 5% (0.05)
+ $answer3 = FinanceProvider::bondYieldToMaturity(922.78, 1000, 0.04, 10);
+ $this->assertEquals('0.05', $answer3->round(4)->getValue());
+ // Verify by calculating bond price with this YTM
+ $verifyPrice3 = FinanceProvider::bondPrice(1000, 0.04, $answer3, 10);
+ $this->assertEquals('922.78', $verifyPrice3->round(2)->getValue());
+
+ // Test case 4: Zero-coupon bond
+ // Price = 613.91, Face Value = 1000, Coupon Rate = 0%, Periods = 10
+ // Expected: YTM = 5% (0.05)
+ // Verification: 1000 / (1.05)^10 = 613.91
+ $answer4 = FinanceProvider::bondYieldToMaturity(613.91, 1000, 0, 10);
+ $this->assertEquals('0.05', $answer4->round(4)->getValue());
+
+ // Test case 5: Larger face value bond
+ // Face Value = 5000, Coupon Rate = 6% (0.06), Market Rate = 6% (0.06), Periods = 10
+ // Expected: YTM = 6% (0.06) (bond at par)
+ $answer5 = FinanceProvider::bondYieldToMaturity(5000, 5000, 0.06, 10);
+ $this->assertEquals('0.06', $answer5->round(4)->getValue());
+ // Verify by calculating bond price with this YTM
+ $verifyPrice5 = FinanceProvider::bondPrice(5000, 0.06, $answer5, 10);
+ $this->assertEquals('5000', $verifyPrice5->round(2)->getValue());
+
+ // Test case 6: High yield bond (trading at significant discount)
+ // Price = 800, Face Value = 1000, Coupon Rate = 5% (0.05), Periods = 10
+ // YTM should be higher than 5%
+ $answer6 = FinanceProvider::bondYieldToMaturity(800, 1000, 0.05, 10);
+ $this->assertGreaterThan(0.05, (float)$answer6->getValue());
+ // Verify by calculating bond price with this YTM
+ $verifyPrice6 = FinanceProvider::bondPrice(1000, 0.05, $answer6, 10);
+ $this->assertEquals('800', $verifyPrice6->round(2)->getValue());
+
+ // Test case 7: Premium bond with different initial guess
+ // Price = 1200, Face Value = 1000, Coupon Rate = 8% (0.08), Periods = 10
+ // Using different initial guess to test convergence
+ $answer7 = FinanceProvider::bondYieldToMaturity(1200, 1000, 0.08, 10, 0.03);
+ // Verify by calculating bond price with this YTM
+ $verifyPrice7 = FinanceProvider::bondPrice(1000, 0.08, $answer7, 10);
+ $this->assertEquals('1200', $verifyPrice7->round(2)->getValue());
+ }
+
+}
diff --git a/vendor b/vendor
new file mode 120000
index 00000000..f42b6d55
--- /dev/null
+++ b/vendor
@@ -0,0 +1 @@
+/home/jordan/Projects/Fermat/vendor
\ No newline at end of file