Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,30 @@ Initial features:

> amortisation

> XIRR (Excel-Compatible)

## XIRR (Excel-Compatible)

Calculate the Extended Internal Rate of Return (XIRR) for irregular cash flows with Excel compatibility.

```fsharp
open System
open FSharp.Finance.Personal

let cashflows = [ DateTime(2025,1,1), -10000m; DateTime(2026,1,1), 11000m ]
let r = Xirr.xirr cashflows
printfn "XIRR = %.4f%%" (r * 100m)
// Output: XIRR = 10.0000%
```

Features:
- **Excel compatibility**: Uses ExcelFinancialFunctions library with default guess=0.1
- **Multiple functions**: `xirr`, `xirrG` (custom guess), `tryXirr` (safe Result type)
- **Input validation**: Ensures mixed signs, sufficient data points, and non-identical dates
- **Decimal precision**: Returns rates as decimal values for financial calculations

The XIRR functions follow Excel's sign convention where negative values represent outflows (investments, payments) and positive values represent inflows (returns, receipts).

This library operates partially in areas where business is regulated by various regulators.
Though every care has been taken to ensure the accuracy of the results, please independently validate the figures produced by it.
It is not audited or validated by any of the regulators.
Expand Down
115 changes: 115 additions & 0 deletions docs/exampleXirr.fsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
(**
# XIRR (Extended Internal Rate of Return) Examples

This example demonstrates the Excel-compatible XIRR functionality provided by the `FSharp.Finance.Personal` library.

## Basic Investment Example

Consider a simple investment scenario where you invest $10,000 and receive $11,000 one year later:
*)

open System
open FSharp.Finance.Personal

// Basic investment: -$10,000 invested today, +$11,000 received in one year
let basicInvestment = [
DateTime(2024, 1, 1), -10000m // Investment outflow
DateTime(2025, 1, 1), 11000m // Return inflow
]

let basicRate = Xirr.xirr basicInvestment
printfn "Basic investment XIRR: %.2f%%" (basicRate * 100m)
// Output: approximately 10.00%

(**
## Salary Advance Example

A salary advance scenario where an employee receives $1,000 today and repays $1,030 in 30 days:
*)

// Salary advance: +$1,000 received today, -$1,030 repaid in 30 days
let salaryAdvance = [
DateTime(2024, 1, 1), 1000m // Advance received (inflow to borrower)
DateTime(2024, 1, 31), -1030m // Repayment (outflow from borrower)
]

let salaryAdvanceRate = Xirr.xirr salaryAdvance
printfn "Salary advance XIRR: %.2f%%" (salaryAdvanceRate * 100m)
// Output: approximately 43.28% (very high due to short term)

(**
## Trade Credit Cashflow Example

A simple trade credit scenario with multiple payments:
*)

// Trade credit: Invoice factoring with advance and final settlement
let tradeCreditCashflows = [
DateTime(2024, 1, 1), -100000m // Invoice amount (outflow to factor)
DateTime(2024, 1, 2), 85000m // Advance payment (inflow from factor)
DateTime(2024, 3, 1), 14000m // Final settlement minus fees (inflow from factor)
]

let tradeCreditRate = Xirr.xirr tradeCreditCashflows
printfn "Trade credit XIRR: %.2f%%" (tradeCreditRate * 100m)
// Output: effective rate for the factoring arrangement

(**
## Using Custom Guess

When the default guess of 0.1 (10%) might not converge well, you can provide a custom initial guess:
*)

// Using a custom guess of 5% instead of the default 10%
let customGuessRate = Xirr.xirrG 0.05m basicInvestment
printfn "Custom guess XIRR: %.2f%%" (customGuessRate * 100m)

(**
## Safe Error Handling

For production code, use `tryXirr` to handle potential calculation failures gracefully:
*)

let safeCalculation cashflows =
match Xirr.tryXirr cashflows with
| Ok rate ->
printfn "XIRR: %.2f%%" (rate * 100m)
| Error message ->
printfn "XIRR calculation failed: %s" message

// Test with valid cashflows
safeCalculation basicInvestment

// Test with invalid cashflows (all positive)
let invalidCashflows = [
DateTime(2024, 1, 1), 1000m
DateTime(2024, 6, 1), 1100m
]
safeCalculation invalidCashflows

(**
## Sign Convention

**Important**: The XIRR calculation follows Excel's sign convention:

- **Negative values**: Money going out (investments, loan disbursements, payments made)
- **Positive values**: Money coming in (returns, loan payments received, income)

From a **borrower's perspective**:
- Loan disbursement: +1000m (money received)
- Loan repayment: -1030m (money paid out)

From an **investor's perspective**:
- Investment: -10000m (money invested)
- Return: +11000m (money received back)

## Excel Compatibility

This implementation uses the `ExcelFinancialFunctions` library to ensure complete compatibility with Excel's XIRR function, including:

- Default guess value of 0.1 (10%)
- Same convergence algorithm
- Identical precision and rounding behavior

The functions return annualized effective rates as decimal values (e.g., 0.10 for 10%).
*)
2 changes: 2 additions & 0 deletions src/FSharp.Finance.Personal.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<Compile Include="Amortisation.fs" />
<Compile Include="Quotes.fs" />
<Compile Include="Refinancing.fs" />
<Compile Include="Xirr.fs" />
<None Include="icon.png" Pack="true" PackagePath="" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
Expand Down Expand Up @@ -53,6 +54,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="9.0.201" />
<PackageReference Include="ExcelFinancialFunctions" Version="3.2.0" />
</ItemGroup>
<PropertyGroup>
<RepositoryUrl>https://github.com/simontreanor/FSharp.Finance.Personal</RepositoryUrl>
Expand Down
89 changes: 89 additions & 0 deletions src/Xirr.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
namespace FSharp.Finance.Personal

open System

/// Excel-compatible XIRR (Extended Internal Rate of Return) calculations
/// using ExcelFinancialFunctions library to ensure parity with Excel results
[<RequireQualifiedAccess>]
module Xirr =

/// Validates that the cashflow list meets XIRR calculation requirements
let private validate (cashflows: (DateTime * decimal) list) =
if cashflows.Length < 2 then
invalidArg (nameof cashflows) "At least two cashflows are required for XIRR calculation"

let values = cashflows |> List.map snd
let hasPositive = values |> List.exists (fun v -> v > 0m)
let hasNegative = values |> List.exists (fun v -> v < 0m)

if not hasPositive then
invalidArg (nameof cashflows) "At least one positive cashflow is required"
if not hasNegative then
invalidArg (nameof cashflows) "At least one negative cashflow is required"

let dates = cashflows |> List.map fst
let uniqueDates = dates |> List.distinct
if uniqueDates.Length = 1 then
invalidArg (nameof cashflows) "Cashflows cannot all have identical dates"

/// Converts decimal cashflows to float sequences for ExcelFinancialFunctions library
let private convertToSequences (cashflows: (DateTime * decimal) list) =
let dates = cashflows |> List.map fst
let values = cashflows |> List.map (fun (_, value) -> float value)
(values, dates)

/// <summary>
/// Calculates the Extended Internal Rate of Return (XIRR) for a series of cashflows.
/// Uses Excel-compatible calculation with default guess of 0.1 (10%).
/// </summary>
/// <param name="cashflows">List of (date, cashflow) pairs.
/// Negative values represent outflows (investments, payments from borrower perspective).
/// Positive values represent inflows (returns, receipts from borrower perspective).</param>
/// <returns>Annualized effective rate as decimal (e.g., 0.10 for 10%)</returns>
/// <remarks>
/// Sign convention follows Excel standard:
/// - Negative cashflows: money going out (investments, loan disbursements)
/// - Positive cashflows: money coming in (returns, loan payments)
/// Default guess of 0.1 ensures Excel compatibility.
/// Precision may be limited by conversion from decimal to float for underlying calculation.
/// </remarks>
let xirr (cashflows: (DateTime * decimal) list) : decimal =
validate cashflows
let (values, dates) = convertToSequences cashflows
let result = Excel.FinancialFunctions.Financial.XIrr(values, dates, 0.1)
decimal result

/// <summary>
/// Calculates the Extended Internal Rate of Return (XIRR) for a series of cashflows
/// with a custom initial guess.
/// </summary>
/// <param name="guess">Initial guess for the XIRR calculation (e.g., 0.1 for 10%)</param>
/// <param name="cashflows">List of (date, cashflow) pairs</param>
/// <returns>Annualized effective rate as decimal</returns>
/// <remarks>
/// Same sign convention as xirr function. Custom guess may improve convergence
/// for some cashflow patterns but should generally not be necessary.
/// </remarks>
let xirrG (guess: decimal) (cashflows: (DateTime * decimal) list) : decimal =
validate cashflows
let (values, dates) = convertToSequences cashflows
let result = Excel.FinancialFunctions.Financial.XIrr(values, dates, float guess)
decimal result

/// <summary>
/// Attempts to calculate the Extended Internal Rate of Return (XIRR) for a series of cashflows,
/// returning a Result type instead of throwing exceptions.
/// </summary>
/// <param name="cashflows">List of (date, cashflow) pairs</param>
/// <returns>Result containing the XIRR rate on success, or error message on failure</returns>
/// <remarks>
/// Uses default guess of 0.1. Returns Result.Error for validation failures or
/// convergence problems in the underlying calculation.
/// </remarks>
let tryXirr (cashflows: (DateTime * decimal) list) : Result<decimal, string> =
try
let result = xirr cashflows
Ok result
with
| :? System.ArgumentException as ex -> Error ex.Message
| ex -> Error $"XIRR calculation failed: {ex.Message}"
1 change: 1 addition & 0 deletions tests/FSharp.Finance.Personal.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<Compile Include="PromotionalRatesTests.fs" />
<Compile Include="QuoteTests.fs" />
<Compile Include="SettlementTests.fs" />
<Compile Include="XirrTests.fs" />
<!-- <Compile Include="UnitPeriodConfigTests.fs" /> -->
</ItemGroup>
<ItemGroup>
Expand Down
97 changes: 97 additions & 0 deletions tests/XirrTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
namespace FSharp.Finance.Personal.Tests

open System
open Xunit
open FsUnit.Xunit
open FSharp.Finance.Personal

module XirrTests =

[<Fact>]
let ``XIRR_basic_one_year should return approximately 10% for simple investment`` () =
let cashflows = [
DateTime(2024, 1, 1), -10000m // Investment outflow
DateTime(2025, 1, 1), 11000m // Return inflow after 1 year
]
let result = Xirr.xirr cashflows
result |> should be (greaterThan 0.095m)
result |> should be (lessThan 0.105m)

[<Fact>]
let ``XIRR_salary_advance_example should return approximately 30-40% for short term loan`` () =
let cashflows = [
DateTime(2024, 1, 1), 1000m // Loan disbursement (inflow to borrower)
DateTime(2024, 1, 31), -1030m // Repayment after 30 days (outflow from borrower)
]
let result = Xirr.xirr cashflows
// Expected around 43% annually for this 3% monthly rate over 30 days
result |> should be (greaterThan 0.30m)
result |> should be (lessThan 0.50m)

[<Fact>]
let ``XIRR_mixed_sign_validation should fail for single sign cashflows`` () =
let positiveCashflows = [
DateTime(2024, 1, 1), 1000m
DateTime(2024, 2, 1), 500m
]
let negativeCashflows = [
DateTime(2024, 1, 1), -1000m
DateTime(2024, 2, 1), -500m
]

let positiveResult = Xirr.tryXirr positiveCashflows
let negativeResult = Xirr.tryXirr negativeCashflows

match positiveResult with
| Error _ -> () // Expected
| Ok _ -> failwith "Expected Error for all positive cashflows"

match negativeResult with
| Error _ -> () // Expected
| Ok _ -> failwith "Expected Error for all negative cashflows"

[<Fact>]
let ``XIRR_guess_consistency should produce similar results`` () =
let cashflows = [
DateTime(2024, 1, 1), -10000m
DateTime(2024, 6, 1), 5000m
DateTime(2025, 1, 1), 6000m
]

let resultDefault = Xirr.xirr cashflows
let resultGuess = Xirr.xirrG 0.1m cashflows

let difference = abs (resultDefault - resultGuess)
difference |> should be (lessThan 1e-10m)

[<Fact>]
let ``XIRR should fail with insufficient cashflows`` () =
let singleCashflow = [DateTime(2024, 1, 1), -1000m]
let emptyCashflows = []

(fun () -> Xirr.xirr singleCashflow |> ignore) |> should throw typeof<System.ArgumentException>
(fun () -> Xirr.xirr emptyCashflows |> ignore) |> should throw typeof<System.ArgumentException>

[<Fact>]
let ``XIRR should fail with identical dates`` () =
let identicalDates = [
DateTime(2024, 1, 1), -1000m
DateTime(2024, 1, 1), 1100m
]

(fun () -> Xirr.xirr identicalDates |> ignore) |> should throw typeof<System.ArgumentException>

[<Fact>]
let ``tryXirr should return Ok for valid cashflows`` () =
let cashflows = [
DateTime(2024, 1, 1), -1000m
DateTime(2025, 1, 1), 1100m
]

let result = Xirr.tryXirr cashflows

match result with
| Ok rate ->
rate |> should be (greaterThan 0.05m)
rate |> should be (lessThan 0.15m)
| Error msg -> failwith $"Expected Ok result but got Error: {msg}"