First pass at extracting Financial Price functions for Securities (#1942)

* Extracting Financial Price functions for Securities - PRICE(), PRICEMAT(), PRICEDISC()
* Additional unit tests for PRICEDISC() invalid arguments
* Additional unit tests for PRICEMAT() invalid arguments
* Add docblock for PRICE()
* Clarification on validation checks for <= 0 and < 0
This commit is contained in:
Mark Baker 2021-03-20 22:52:04 +01:00 committed by GitHub
parent d346318c2b
commit b87d78b206
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 426 additions and 146 deletions

View File

@ -1983,17 +1983,17 @@ class Calculation
],
'PRICE' => [
'category' => Category::CATEGORY_FINANCIAL,
'functionCall' => [Financial::class, 'PRICE'],
'functionCall' => [Financial\Securities::class, 'price'],
'argumentCount' => '6,7',
],
'PRICEDISC' => [
'category' => Category::CATEGORY_FINANCIAL,
'functionCall' => [Financial::class, 'PRICEDISC'],
'functionCall' => [Financial\Securities::class, 'discounted'],
'argumentCount' => '4,5',
],
'PRICEMAT' => [
'category' => Category::CATEGORY_FINANCIAL,
'functionCall' => [Financial::class, 'PRICEMAT'],
'functionCall' => [Financial\Securities::class, 'maturity'],
'argumentCount' => '5,6',
],
'PROB' => [

View File

@ -11,15 +11,6 @@ class Financial
const FINANCIAL_PRECISION = 1.0e-08;
private static function isValidFrequency($frequency)
{
if (($frequency == 1) || ($frequency == 2) || ($frequency == 4)) {
return true;
}
return false;
}
private static function interestAndPrincipal($rate = 0, $per = 0, $nper = 0, $pv = 0, $fv = 0, $type = 0)
{
$pmt = self::PMT($rate, $nper, $pv, $fv, $type);
@ -1370,79 +1361,39 @@ class Financial
return $interestAndPrincipal[1];
}
private static function validatePrice($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis)
{
if (is_string($settlement)) {
return Functions::VALUE();
}
if (is_string($maturity)) {
return Functions::VALUE();
}
if (!is_numeric($rate)) {
return Functions::VALUE();
}
if (!is_numeric($yield)) {
return Functions::VALUE();
}
if (!is_numeric($redemption)) {
return Functions::VALUE();
}
if (!is_numeric($frequency)) {
return Functions::VALUE();
}
if (!is_numeric($basis)) {
return Functions::VALUE();
}
return '';
}
/**
* PRICE.
*
* Returns the price per $100 face value of a security that pays periodic interest.
*
* @Deprecated 1.18.0
*
* @see Use the price() method in the Financial\Securities class instead
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue date when the security
* is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param float $rate the security's annual coupon rate
* @param float $yield the security's annual yield
* @param float $redemption The number of coupon payments per year.
* For annual payments, frequency = 1;
* for semiannual, frequency = 2;
* for quarterly, frequency = 4.
* @param int $frequency
* @param int $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function PRICE($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis = 0)
{
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$rate = Functions::flattenSingleValue($rate);
$yield = Functions::flattenSingleValue($yield);
$redemption = Functions::flattenSingleValue($redemption);
$frequency = Functions::flattenSingleValue($frequency);
$basis = Functions::flattenSingleValue($basis);
$settlement = DateTime::getDateValue($settlement);
$maturity = DateTime::getDateValue($maturity);
$rslt = self::validatePrice($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis);
if ($rslt) {
return $rslt;
}
$rate = (float) $rate;
$yield = (float) $yield;
$redemption = (float) $redemption;
$frequency = (int) $frequency;
$basis = (int) $basis;
if (
($settlement > $maturity) ||
(!self::isValidFrequency($frequency)) ||
(($basis < 0) || ($basis > 4))
) {
return Functions::NAN();
}
$dsc = self::COUPDAYSNC($settlement, $maturity, $frequency, $basis);
$e = self::COUPDAYS($settlement, $maturity, $frequency, $basis);
$n = self::COUPNUM($settlement, $maturity, $frequency, $basis);
$a = self::COUPDAYBS($settlement, $maturity, $frequency, $basis);
$baseYF = 1.0 + ($yield / $frequency);
$rfp = 100 * ($rate / $frequency);
$de = $dsc / $e;
$result = $redemption / $baseYF ** (--$n + $de);
for ($k = 0; $k <= $n; ++$k) {
$result += $rfp / ($baseYF ** ($k + $de));
}
$result -= $rfp * ($a / $e);
return $result;
return Financial\Securities::price($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis);
}
/**
@ -1450,6 +1401,10 @@ class Financial
*
* Returns the price per $100 face value of a discounted security.
*
* @Deprecated 1.18.0
*
* @see Use the discounted() method in the Financial\Securities class instead
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue date when the security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
@ -1467,27 +1422,7 @@ class Financial
*/
public static function PRICEDISC($settlement, $maturity, $discount, $redemption, $basis = 0)
{
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$discount = (float) Functions::flattenSingleValue($discount);
$redemption = (float) Functions::flattenSingleValue($redemption);
$basis = (int) Functions::flattenSingleValue($basis);
// Validate
if ((is_numeric($discount)) && (is_numeric($redemption)) && (is_numeric($basis))) {
if (($discount <= 0) || ($redemption <= 0)) {
return Functions::NAN();
}
$daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis);
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
return $redemption * (1 - $discount * $daysBetweenSettlementAndMaturity);
}
return Functions::VALUE();
return Financial\Securities::discounted($settlement, $maturity, $discount, $redemption, $basis);
}
/**
@ -1495,6 +1430,10 @@ class Financial
*
* Returns the price per $100 face value of a security that pays interest at maturity.
*
* @Deprecated 1.18.0
*
* @see Use the maturity() method in the Financial\Securities class instead
*
* @param mixed $settlement The security's settlement date.
* The security's settlement date is the date after the issue date when the security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
@ -1513,47 +1452,7 @@ class Financial
*/
public static function PRICEMAT($settlement, $maturity, $issue, $rate, $yield, $basis = 0)
{
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$issue = Functions::flattenSingleValue($issue);
$rate = Functions::flattenSingleValue($rate);
$yield = Functions::flattenSingleValue($yield);
$basis = (int) Functions::flattenSingleValue($basis);
// Validate
if (is_numeric($rate) && is_numeric($yield)) {
if (($rate <= 0) || ($yield <= 0)) {
return Functions::NAN();
}
$daysPerYear = Financial\Helpers::daysPerYear(DateTime::YEAR($settlement), $basis);
if (!is_numeric($daysPerYear)) {
return $daysPerYear;
}
$daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis);
if (!is_numeric($daysBetweenIssueAndSettlement)) {
// return date error
return $daysBetweenIssueAndSettlement;
}
$daysBetweenIssueAndSettlement *= $daysPerYear;
$daysBetweenIssueAndMaturity = DateTime::YEARFRAC($issue, $maturity, $basis);
if (!is_numeric($daysBetweenIssueAndMaturity)) {
// return date error
return $daysBetweenIssueAndMaturity;
}
$daysBetweenIssueAndMaturity *= $daysPerYear;
$daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis);
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
$daysBetweenSettlementAndMaturity *= $daysPerYear;
return (100 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate * 100)) /
(1 + (($daysBetweenSettlementAndMaturity / $daysPerYear) * $yield)) -
(($daysBetweenIssueAndSettlement / $daysPerYear) * $rate * 100);
}
return Functions::VALUE();
return Financial\Securities::maturity($settlement, $maturity, $issue, $rate, $yield, $basis);
}
/**

View File

@ -419,7 +419,7 @@ class Coupons
return $frequency;
}
private static function validateBasis($basis)
private static function validateBasis($basis): int
{
if (!is_numeric($basis)) {
throw new Exception(Functions::NAN());

View File

@ -0,0 +1,321 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use PhpOffice\PhpSpreadsheet\Calculation\DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Securities
{
public const FREQUENCY_ANNUAL = 1;
public const FREQUENCY_SEMI_ANNUAL = 2;
public const FREQUENCY_QUARTERLY = 4;
/**
* PRICE.
*
* Returns the price per $100 face value of a security that pays periodic interest.
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue date when the security
* is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param float $rate the security's annual coupon rate
* @param float $yield the security's annual yield
* @param float $redemption The number of coupon payments per year.
* For annual payments, frequency = 1;
* for semiannual, frequency = 2;
* for quarterly, frequency = 4.
* @param int $frequency
* @param int $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function price($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis = 0)
{
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$rate = Functions::flattenSingleValue($rate);
$yield = Functions::flattenSingleValue($yield);
$redemption = Functions::flattenSingleValue($redemption);
$frequency = Functions::flattenSingleValue($frequency);
$basis = Functions::flattenSingleValue($basis);
try {
$settlement = self::validateSettlementDate($settlement);
$maturity = self::validateMaturityDate($maturity);
self::validateSecurityPeriod($settlement, $maturity);
$rate = self::validateRate($rate);
$yield = self::validateYield($yield);
$redemption = self::validateRedemption($redemption);
$frequency = self::validateFrequency($frequency);
$basis = self::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$dsc = Coupons::COUPDAYSNC($settlement, $maturity, $frequency, $basis);
$e = Coupons::COUPDAYS($settlement, $maturity, $frequency, $basis);
$n = Coupons::COUPNUM($settlement, $maturity, $frequency, $basis);
$a = Coupons::COUPDAYBS($settlement, $maturity, $frequency, $basis);
$baseYF = 1.0 + ($yield / $frequency);
$rfp = 100 * ($rate / $frequency);
$de = $dsc / $e;
$result = $redemption / $baseYF ** (--$n + $de);
for ($k = 0; $k <= $n; ++$k) {
$result += $rfp / ($baseYF ** ($k + $de));
}
$result -= $rfp * ($a / $e);
return $result;
}
/**
* PRICEDISC.
*
* Returns the price per $100 face value of a discounted security.
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue date when the security
* is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param float $discount The security's discount rate
* @param float $redemption The security's redemption value per $100 face value
* @param int $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function discounted($settlement, $maturity, $discount, $redemption, $basis = 0)
{
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$discount = Functions::flattenSingleValue($discount);
$redemption = Functions::flattenSingleValue($redemption);
$basis = Functions::flattenSingleValue($basis);
try {
$settlement = self::validateSettlementDate($settlement);
$maturity = self::validateMaturityDate($maturity);
self::validateSecurityPeriod($settlement, $maturity);
$discount = self::validateDiscount($discount);
$redemption = self::validateRedemption($redemption);
$basis = self::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis);
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
return $redemption * (1 - $discount * $daysBetweenSettlementAndMaturity);
}
/**
* PRICEMAT.
*
* Returns the price per $100 face value of a security that pays interest at maturity.
*
* @param mixed $settlement The security's settlement date.
* The security's settlement date is the date after the issue date when the
* security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $issue The security's issue date
* @param float $rate The security's interest rate at date of issue
* @param float $yield The security's annual yield
* @param int $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return float|string Result, or a string containing an error
*/
public static function maturity($settlement, $maturity, $issue, $rate, $yield, $basis = 0)
{
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$issue = Functions::flattenSingleValue($issue);
$rate = Functions::flattenSingleValue($rate);
$yield = Functions::flattenSingleValue($yield);
$basis = Functions::flattenSingleValue($basis);
try {
$settlement = self::validateSettlementDate($settlement);
$maturity = self::validateMaturityDate($maturity);
self::validateSecurityPeriod($settlement, $maturity);
$issue = self::validateIssueDate($issue);
$rate = self::validateRate($rate);
$yield = self::validateYield($yield);
$basis = self::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis);
if (!is_numeric($daysPerYear)) {
return $daysPerYear;
}
$daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis);
if (!is_numeric($daysBetweenIssueAndSettlement)) {
// return date error
return $daysBetweenIssueAndSettlement;
}
$daysBetweenIssueAndSettlement *= $daysPerYear;
$daysBetweenIssueAndMaturity = DateTime::YEARFRAC($issue, $maturity, $basis);
if (!is_numeric($daysBetweenIssueAndMaturity)) {
// return date error
return $daysBetweenIssueAndMaturity;
}
$daysBetweenIssueAndMaturity *= $daysPerYear;
$daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis);
if (!is_numeric($daysBetweenSettlementAndMaturity)) {
// return date error
return $daysBetweenSettlementAndMaturity;
}
$daysBetweenSettlementAndMaturity *= $daysPerYear;
return (100 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate * 100)) /
(1 + (($daysBetweenSettlementAndMaturity / $daysPerYear) * $yield)) -
(($daysBetweenIssueAndSettlement / $daysPerYear) * $rate * 100);
}
private static function validateInputDate($date)
{
$date = DateTime::getDateValue($date);
if (is_string($date)) {
throw new Exception(Functions::VALUE());
}
return $date;
}
private static function validateSettlementDate($settlement)
{
return self::validateInputDate($settlement);
}
private static function validateMaturityDate($maturity)
{
return self::validateInputDate($maturity);
}
private static function validateIssueDate($issue)
{
return self::validateInputDate($issue);
}
private static function validateSecurityPeriod($settlement, $maturity): void
{
if ($settlement >= $maturity) {
throw new Exception(Functions::NAN());
}
}
private static function validateRate($rate): float
{
if (!is_numeric($rate)) {
throw new Exception(Functions::VALUE());
}
$rate = (float) $rate;
if ($rate < 0.0) {
throw new Exception(Functions::NAN());
}
return $rate;
}
private static function validateYield($yield): float
{
if (!is_numeric($yield)) {
throw new Exception(Functions::VALUE());
}
$yield = (float) $yield;
if ($yield < 0.0) {
throw new Exception(Functions::NAN());
}
return $yield;
}
private static function validateRedemption($redemption): float
{
if (!is_numeric($redemption)) {
throw new Exception(Functions::VALUE());
}
$redemption = (float) $redemption;
if ($redemption <= 0.0) {
throw new Exception(Functions::NAN());
}
return $redemption;
}
private static function validateDiscount($discount): float
{
if (!is_numeric($discount)) {
throw new Exception(Functions::VALUE());
}
$discount = (float) $discount;
if ($discount <= 0.0) {
throw new Exception(Functions::NAN());
}
return $discount;
}
private static function validateFrequency($frequency): int
{
if (!is_numeric($frequency)) {
throw new Exception(Functions::VALUE());
}
$frequency = (int) $frequency;
if (
($frequency !== self::FREQUENCY_ANNUAL) &&
($frequency !== self::FREQUENCY_SEMI_ANNUAL) &&
($frequency !== self::FREQUENCY_QUARTERLY)
) {
throw new Exception(Functions::NAN());
}
return $frequency;
}
private static function validateBasis($basis): int
{
if (!is_numeric($basis)) {
throw new Exception(Functions::VALUE());
}
$basis = (int) $basis;
if (($basis < 0) || ($basis > 4)) {
throw new Exception(Functions::NAN());
}
return $basis;
}
}

View File

@ -9,4 +9,36 @@ return [
97.6311475409836,
['2008-02-15', '2008-11-30', 0.03, 100, 1],
],
[
'#VALUE!',
['Invalid Date', '2008-11-30', 0.03, 100, 1],
],
[
'#VALUE!',
['2008-02-15', 'Invalid Date', 0.03, 100, 1],
],
[
'#VALUE!',
['2008-02-15', '2008-11-30', 'NaN', 100, 1],
],
[
'#NUM!',
['2008-02-15', '2008-11-30', -0.03, 100, 1],
],
[
'#VALUE!',
['2008-02-15', '2008-11-30', 0.03, 'NaN', 1],
],
[
'#NUM!',
['2008-02-15', '2008-11-30', 0.03, -100, 1],
],
[
'#VALUE!',
['2008-02-15', '2008-11-30', 0.03, 100, 'NaN'],
],
[
'#NUM!',
['2008-02-15', '2008-11-30', 0.03, 100, -1],
],
];

View File

@ -13,4 +13,32 @@ return [
93.0909090909091,
'1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 0.07, 0.08,
],
[
'#VALUE!',
'Invalid Date', '1-Jan-2027', '1-Jan-2017', 0.07, 0.08,
],
[
'#VALUE!',
'1-Jul-2017', 'Invalid Date', '1-Jan-2017', 0.07, 0.08,
],
[
'#VALUE!',
'1-Jul-2017', '1-Jan-2027', 'Invalid Date', 0.07, 0.08,
],
[
'#VALUE!',
'1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 'NaN', 0.08,
],
[
'#NUM!',
'1-Jul-2017', '1-Jan-2027', '1-Jan-2017', -0.07, 0.08,
],
[
'#VALUE!',
'1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 0.07, 'NaN',
],
[
'#NUM!',
'1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 0.07, -0.08,
],
];