From 96573990d0d0f13a2f57e48b3e1e876e7b6434a6 Mon Sep 17 00:00:00 2001 From: Todd Vrba Date: Fri, 2 Feb 2024 04:00:37 -0600 Subject: [PATCH] Fix case where implied volatility is outside the initial range (#2) Co-authored-by: Zois Pagoulatos --- composer.json | 5 ++++- src/Black76.php | 17 ++++++++++++++--- tests/Black76Test.php | 10 +++++++--- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index c5bd61b..ccac427 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,10 @@ "format": "vendor/bin/php-cs-fixer fix --config .php_cs.dist.php --allow-risky=yes" }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } }, "minimum-stability": "dev", "prefer-stable": true diff --git a/src/Black76.php b/src/Black76.php index b90c1b8..7703f10 100644 --- a/src/Black76.php +++ b/src/Black76.php @@ -3,6 +3,7 @@ namespace Kyos\OptionsCalculator; use Exception; +use RuntimeException; class Black76 { @@ -15,7 +16,7 @@ class Black76 private float $interestRate; /** - * @param float $interestRate Annual risk-free nterest rate, defaults to 0.01 (1%) + * @param float $interestRate Annual risk-free interest rate, defaults to 0.01 (1%) */ public function __construct(float $interestRate = 0.01) { @@ -99,6 +100,8 @@ public function getValues( * @param float $marketPrice Option price * * @return float + * + * @throws RuntimeException */ private function impliedVolaUsingBisection( string $type, @@ -113,8 +116,16 @@ private function impliedVolaUsingBisection( $volMin = 0.00001; $volMax = 5; + $volGuess = $volMin; + + $valueMin = $this->getValues($type, $underlyingPrice, $strikePrice, $timeToMaturity, $volMin)['value']; + $valueMax = $this->getValues($type, $underlyingPrice, $strikePrice, $timeToMaturity, $volMax)['value']; - do { + if ($marketPrice < $valueMin || $marketPrice > $valueMax) { + throw new RuntimeException("Implied volatility could not be found in the range {$volMin} - {$volMax}."); + } + + while (++$i < $maxIterations && ($volMax - $volMin > $epsilon || $valueMin != $valueMax)) { $valueMin = $this->getValues($type, $underlyingPrice, $strikePrice, $timeToMaturity, $volMin)['value']; $valueMax = $this->getValues($type, $underlyingPrice, $strikePrice, $timeToMaturity, $volMax)['value']; @@ -126,7 +137,7 @@ private function impliedVolaUsingBisection( } else { $volMax = $volGuess; } - } while (++$i <= $maxIterations && $volMax - $volMin > $epsilon); + } return $volGuess; } diff --git a/tests/Black76Test.php b/tests/Black76Test.php index 4cfa38c..1a6d800 100644 --- a/tests/Black76Test.php +++ b/tests/Black76Test.php @@ -34,11 +34,15 @@ $this->expect((new Black76())->getImpliedVolatility(Black76::PUT, 10.5, 12, 30 / 365.25, 1.7398186455107651))->toEqualWithDelta(0.60, 0.00000000000001); }); -// Expections -it('throws expection if we try to derive implied volatility using newton rapshon', function () { +it('throws exception if implied volatility is outside the initial range', function () { + (new Black76())->getImpliedVolatility(Black76::PUT, 10.5, 12, 30 / 365.25, 1); +})->throws(RuntimeException::class); + +// Exceptions +it('throws exception if we try to derive implied volatility using newton rapshon', function () { (new Black76())->getImpliedVolatility(Black76::PUT, 10.5, 12, 30 / 365.25, 0.240, Black76::METHOD_NEWTON_RAPHSON); })->throws('Not implemented'); -it('throws expection if we try to derive implied volatility using wrong method', function () { +it('throws exception if we try to derive implied volatility using wrong method', function () { (new Black76())->getImpliedVolatility(Black76::PUT, 10.5, 12, 30 / 365.25, 0.240, 99); })->throws('Wrong method 99 or not supported');