Skip to content
Merged
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
7 changes: 6 additions & 1 deletion src/ExcelFinancialFunctions/bonds.fs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ module internal Bonds =
accrIntM issue settlement rate par basis
let calcAccrInt issue firstInterest settlement rate par (frequency:Frequency) basis (calcMethod:AccrIntCalcMethod) =
(settlement > issue) |> elseThrow "settlement must be after issue"
(firstInterest > settlement) |> elseThrow "firstInterest must be after settlement"
(firstInterest >= settlement) |> elseThrow "firstInterest must be after settlement"
(rate > 0.) |> elseThrow "rate must be more than 0"
(par > 0.) |> elseThrow "par must be more than 0"
accrInt issue firstInterest settlement rate par frequency basis calcMethod
Expand All @@ -158,6 +158,11 @@ module internal Bonds =
(yld > 0.) |> elseThrow "yld must be more than 0"
(redemption > 0.) |> elseThrow "redemption must be more than 0"
price settlement maturity rate yld redemption (float (int frequency)) basis
let calcPriceAllowNegativeYield settlement maturity rate yld redemption (frequency:Frequency) basis =
(maturity > settlement) |> elseThrow "maturity must be after settlement"
(rate >= 0.) |> elseThrow "rate must not be negative"
(redemption > 0.) |> elseThrow "redemption must be more than 0"
price settlement maturity rate yld redemption (float (int frequency)) basis
let calcYield settlement maturity rate pr redemption (frequency:Frequency) basis =
(maturity > settlement) |> elseThrow "maturity must be after settlement"
(rate >= 0.) |> elseThrow "rate must not be negative"
Expand Down
2 changes: 1 addition & 1 deletion src/ExcelFinancialFunctions/common.fs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ module internal Common =
// If the result is sensible (it exist and has the same sign as guess), then return it, else try bisection.
// I'm sure more complex way to pick algos exist (i.e. Brent's method). But I favor simplicity here ...
let findRoot f guess =
let precision = 0.0000001 // Excel precision on this, from docs
let precision = 0.000001 // Excel precision on this, from docs
let newtValue = newton f guess 0 precision
if newtValue.IsSome && sign guess = sign newtValue.Value
then newtValue.Value
Expand Down
14 changes: 13 additions & 1 deletion src/ExcelFinancialFunctions/tvm.fs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ module internal Tvm =
- (fv + pv) / pmt
else
nper r pmt pv fv pd
let calcRri nper pv fv =
( nper > 0. ) |> elseThrow "nper must be > 0"
if fv = pv then 0.
else
( pv <> 0. ) |> elseThrow "pv must be non-zero unless fv is zero"
( fv/pv >= 0. ) |> elseThrow "fv and pv must have same sign"
( pow (fv/pv) (1.0/nper) ) - 1.
let calcRate nper pmt pv fv pd guess =
let haveRightSigns x y z =
not( sign x = sign y && sign y = sign z) &&
Expand All @@ -63,4 +70,9 @@ module internal Tvm =
let calcFvSchedule (pv:float) interests =
let mutable result = pv
for i in interests do result <- result * (1. + i)
result
result
let calcPduration rate pv fv =
( rate > 0. ) |> elseThrow "rate must be positive"
( pv > 0. ) |> elseThrow "pv must be positive"
( fv > 0. ) |> elseThrow "fv must be positive"
( (log fv) - (log pv) ) / log ( 1. + rate)
17 changes: 16 additions & 1 deletion src/ExcelFinancialFunctions/wrapperdotnettype.fs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ type Financial =
static member Price (settlement, maturity, rate, yld, redemption, frequency, basis) =
calcPrice settlement maturity rate yld redemption frequency basis

/// The price per $100 face value of a security that pays periodic interest
/// This is the same calculation as "Price", but allows a negative yield. Excel does not allow negative yield, so this is an addition to the Excel-
/// compatible UI
static member PriceAllowNegativeYield (settlement, maturity, rate, yld, redemption, frequency, basis) =
calcPriceAllowNegativeYield settlement maturity rate yld redemption frequency basis

/// <a target="_blank" href="https://support.microsoft.com/en-us/office/pricedisc-function-d06ad7c1-380e-4be7-9fd9-75e3079acfd3">PRICEDISC function</a>
/// The price per $100 face value of a discounted security
static member PriceDisc (settlement, maturity, discount, redemption, basis) =
Expand Down Expand Up @@ -343,4 +349,13 @@ type Financial =
/// Calculates the fraction of the year represented by the number of whole days between two dates - not a financial function
static member YearFrac (startDate, endDate, basis) =
calcYearFrac startDate endDate basis


/// <a target="_blank" href="https://support.microsoft.com/en-us/office/rri-function-6f5822d8-7ef1-4233-944c-79e8172930f4">RRI function</a>
/// Returns an equivalent interest rate for the growth of an investment
static member Rri (nper, pv, fv) =
calcRri nper pv fv

/// <a target="_blank" href="https://support.microsoft.com/en-us/office/pduration-function-44f33460-5be5-4c90-b857-22308892adaf">PDURATION function</a>
/// Returns the number of periods required by an investment to reach a specified value.
static member Pduration (rate, pv, fv) =
calcPduration rate pv fv
74 changes: 74 additions & 0 deletions tests/ExcelFinancialFunctions.Tests/crosstests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,85 @@
namespace Excel.FinancialFunctions.Tests

open NUnit.Framework
open System.IO
open System

[<SetCulture("en-US")>]
module CrossTests =
open Excel.FinancialFunctions

// I am prototyping a new way of doing cross tests. The idea is to let the NUnit framework
// do more of the work. So we read in the test data into a TestCaseData object, and then
// let NUnit work with that as normal.
//
// Also these files will be slightly different. They'll have a header, because I exported
// them from Excel, and they'll have .csv file extension.

let readTestCaseData fname goodcases =
Path.Combine(__SOURCE_DIRECTORY__, "testdata", fname + ".csv")
|> File.ReadLines
|> Seq.tail // Skip header
|> Seq.filter (fun line -> not (String.IsNullOrEmpty line))
|> Seq.filter (fun line -> line.Contains("#NUM!") <> goodcases )
|> Seq.map (fun line -> line.Split [| ',' |] )
|> Seq.map TestCaseData

let inline shouldEqual exp act =
Assert.AreEqual(exp, float act, PRECISION)

let inline elseThrow s c = if not(c) then failwith s

let pduration_testdata_fromfile =
readTestCaseData "pduration" true

[<TestCaseSource( nameof pduration_testdata_fromfile)>]
let pduration inputs =
let (param,expected) = parse4 inputs
Financial.Pduration param
|> shouldEqual expected

let pduration_failures_fromfile =
readTestCaseData "pduration" false

[<TestCaseSource( nameof pduration_failures_fromfile)>]
let pduration_fail inputs =
let (param,expected) = parse4 inputs
( expected = 0.0 ) |> elseThrow "Failure test must not have an expected value"
Assert.Throws(fun () -> Financial.Pduration param |> ignore) |> ignore

let rri_testdata_fromfile =
readTestCaseData "rri" true

[<TestCaseSource( nameof rri_testdata_fromfile)>]
let rri inputs =
let (param,expected) = parse4 inputs
Financial.Rri param
|> shouldEqual expected

let rri_failures_fromfile =
readTestCaseData "rri" false

[<TestCaseSource( nameof rri_failures_fromfile)>]
let rri_fail inputs =
let (param,expected) = parse4 inputs
( expected = 0.0 ) |> elseThrow "Failure test must not have an expected value"
Assert.Throws(fun () -> Financial.Rri param |> ignore) |> ignore

// Prices for negative yielding bonds match reverse-figured Excel results to 1e-6 until yields get lower than -0.05%
// Down to -1.0%, the difference is usually pennies. Once the yield reaches -10%, the difference can be up to $150, but let's hope
// we never get there!
//
// See the file yieldnegativefails.csv for the cases which fail

let yieldnegative_testdata_fromfile =
readTestCaseData "yieldnegative" true

[<TestCaseSource( nameof yieldnegative_testdata_fromfile)>]
let yieldnegative inputs =
let (param,expected) = parse8 inputs
Financial.PriceAllowNegativeYield param
|> shouldEqual expected

[<Test>]
let accrint() = runTests "accrint" parse8 Financial.AccrInt

Expand Down
11 changes: 9 additions & 2 deletions tests/ExcelFinancialFunctions.Tests/spottests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ module SpotTests =
Financial.Yield param
|> shouldEqual (sprintf "YieldIssue8(%A)" param) -0.67428578540657702

[<Test>]
let XirrIssue27() =
let values = [ -177900000.; 8799805.85 ]
let dates = [ DateTime(2020,7,3); DateTime(2021,2,25) ]
Financial.XIrr (values, dates)
|> shouldEqual (sprintf "XirrIssue27(%A,%A)" values dates) -0.990247691899517

[<Test>]
let spotYield() =
let param =
Expand All @@ -30,7 +37,7 @@ module SpotTests =
|> shouldEqual (sprintf "xnpv(%A)" param) 1.375214

[<Test>]
[<Ignore("This test fails intermittently")>]
[<Explicit("This test fails intermittently")>]
let ``duration shouldn't be greater than maturity``() =
fsCheck (fun (sd: DateTime) yrs cpn' yld' freq basis ->
let md, cpn, yld = sd.AddYears yrs, toFloat cpn', toFloat yld'
Expand All @@ -41,7 +48,7 @@ module SpotTests =
duration - float yrs < PRECISION))

[<Test>]
[<Ignore("This test fails intermittently")>]
[<Explicit("This test fails intermittently")>]
let ``mduration shouldn't be greater than maturity``() =
fsCheck (fun (sd: DateTime) yrs cpn' yld' freq basis ->
let md, cpn, yld = sd.AddYears yrs, toFloat cpn', toFloat yld'
Expand Down
1 change: 1 addition & 0 deletions tests/ExcelFinancialFunctions.Tests/testdata/accrint.test
Original file line number Diff line number Diff line change
Expand Up @@ -1920,3 +1920,4 @@
4/2/1999 12:00:00 AM,1/2/2002 12:00:00 AM,7/2/2000 12:00:00 AM,0.1,12030.34,Quarterly,UsPsa30_360,1503.7925
3/4/1984 12:00:00 AM,3/4/1994 12:00:00 AM,4/5/1991 12:00:00 AM,0.07,120,Quarterly,UsPsa30_360,59.52333333333
3/4/1984 12:00:00 AM,3/4/1994 12:00:00 AM,4/5/1991 12:00:00 AM,0.07,120,Quarterly,UsPsa30_360,59.52333333333
8/15/2018 12:00:00 AM,2/15/2019 12:00:00 AM,2/15/2019 12:00:00 AM,0.02125,100,SemiAnnual,ActualActual,1.0625
24 changes: 24 additions & 0 deletions tests/ExcelFinancialFunctions.Tests/testdata/pduration.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
rate,pv,fv,pduration
0.01530947049973120,-5,-6,#NUM!
-1.00000000000000000,-5,0,#NUM!
0.00000000000000000,-1,-1,#NUM!
0.00000000000000000,300,300,#NUM!
0.10000000000000000,0,100,#NUM!
0.10000000000000000,100,0,#NUM!
0.02426318074098920,300,400,12
0.24092317318260100,300,4000,12
0.50341274654387500,300,40000,12
0.01205888205231860,300,400,24
0.11396731243901500,300,4000,24
0.22613732776711200,300,40000,24
0.00759931015463056,300,400,38
0.07054185347032280,300,4000,38
0.13741628093790000,300,40000,38
0.98822504304098700,10000,2441880,8
0.04663513939210560,5000,6000,4
0.18920711500272100,5000,10000,4
0.10000000000000000,250,275,1
0.41421356237309500,250,500,2
0.52118098430455700,250,880,3
0.02500000000000000,2000,2200,3.85986616262266
0.00208333333333333,1000,1200,87.60547641937140
32 changes: 32 additions & 0 deletions tests/ExcelFinancialFunctions.Tests/testdata/rri.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
nper,pv,fv,rri
0,300,400,#NUM!
0,-1,-3,#NUM!
1,-1,-3,2
12,100,10,-0.174595815
12,100,-90,#NUM!
5,0,0,0
5,-1,5,#NUM!
5,10,10,0
2,2,8,1
2,8,2,-0.5
2,8,0,-1
2,0,10,#NUM!
12,-5,-6,0.01530947
1,-5,0,-1
12,-1,-1,0
12,300,300,0
12,300,400,0.024263181
12,300,4000,0.240923173
12,300,40000,0.503412747
24,300,400,0.012058882
24,300,4000,0.113967312
24,300,40000,0.226137328
38,300,400,0.00759931
38,300,4000,0.070541853
38,300,40000,0.137416281
8,10000,2441880,0.988225043
4,5000,6000,0.046635139
4,5000,10000,0.189207115
1,250,275,0.1
2,250,500,0.414213562
3,250,880,0.521180984
Binary file not shown.
Loading