Skip to content

Commit

Permalink
fix: add logic handling zero strike or low strike scenarios (#12)
Browse files Browse the repository at this point in the history
* fix: add logic handling zero strike or low strike scenarios

* chore: rename tao -> tau

* chore: implement new rule, rename more taos to tau

* chore: clean compiler warnings

* chore: new formula

* build: add new formula and refactor K calculation

* chore: cleanup getK

* chore: update fuzz test descriptions

* chore: review fixes

* chore: renaming
  • Loading branch information
antoncoding authored Sep 12, 2023
1 parent f01ead7 commit 5c38c1b
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 17 deletions.
8 changes: 6 additions & 2 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@ QuickSortTest:testSortEven() (gas: 22054)
QuickSortTest:testSortOdd() (gas: 17712)
QuickSortTest:testSortWithDuplicates() (gas: 16076)
QuickSortTest:testWrostCaseGasArrayOf30() (gas: 68523)
SVITest:testGetVols() (gas: 79850)
SVITest:testRevertsForBadParams() (gas: 13999)
SVITest:testFuzzGetVol(uint256,uint256,uint64) (runs: 256, μ: 19756, ~: 20176)
SVITest:testGetVols() (gas: 106914)
SVITest:testRevertWhenForwardPriceIsZero() (gas: 9476)
SVITest:testRevertsForBadParams() (gas: 10958)
SVITest:testSmallStrike() (gas: 14768)
SVITest:testZeroStrikeVolIsZero() (gas: 6808)
SignedDecimalMathTest:testConstants() (gas: 10476)
SignedDecimalMathTest:testConversion() (gas: 7989)
SignedDecimalMathTest:testDiv() (gas: 7590)
Expand Down
50 changes: 42 additions & 8 deletions src/math/SVI.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "src/decimals/SignedDecimalMath.sol";
import "src/decimals/DecimalMath.sol";
import "./FixedPointMathLib.sol";

import "forge-std/console2.sol";
import "openzeppelin-upgradeable/utils/math/SafeCastUpgradeable.sol";

/**
* @title SVI
Expand All @@ -18,8 +18,17 @@ library SVI {
using SignedDecimalMath for int;
using FixedPointMathLib for uint;
using FixedPointMathLib for int;
using SafeCastUpgradeable for int;
using SafeCastUpgradeable for uint;

error SVI_InvalidParameters();
error SVI_NoForwardPrice();

/// @dev Upper bound of of w in SVI
int internal constant MAX_TOTAL_VAR = 4e18;

/// @dev scaler used to calculate the upper bound and lower bound of k in SVI
int internal constant TOTAL_VOL_SCALAR = 4e18;

/**
* @dev compute the vol for a given strike and set of SVI parameters
Expand All @@ -30,22 +39,24 @@ library SVI {
* @param m SVI parameter in range (-inf, inf)
* @param sigma SVI parameter in range (0, inf)
* @param forwardPrice forward price in range [0, inf)
* @param tao time to expiry (in years) in range [0, inf)
* @param tau time to expiry (in years) in range [0, inf)
* @return vol
*/
function getVol(uint strike, int a, uint b, int rho, int m, uint sigma, uint forwardPrice, uint64 tao)
function getVol(uint strike, int a, uint b, int rho, int m, uint sigma, uint forwardPrice, uint64 tau)
internal
pure
returns (uint)
{
// k = ln(strike / fwd)
int k = FixedPointMathLib.ln(int(strike.divideDecimal(forwardPrice)));
if (strike == 0) return 0;
if (forwardPrice == 0) revert SVI_NoForwardPrice();

// k = ln(strike / fwd) but with some bounds
int k = getK(strike, a, b, sigma, forwardPrice);
int k_sub_m = int(k) - m;

// any number squared is positive, so we can cast to uint
uint k_sub_m_sq = uint(k_sub_m.multiplyDecimal(k_sub_m)); // (k - m)^2
uint sigma_sq = uint(sigma.multiplyDecimal(sigma)); // sigma^2
uint sigma_sq = sigma.multiplyDecimal(sigma); // sigma^2

// b * (sqrt((k - m)^2 + sigma^2) + rho * (k - m))
int bPortion =
Expand All @@ -56,9 +67,32 @@ library SVI {

if (w < 0) {
revert SVI_InvalidParameters();
} else if (w > MAX_TOTAL_VAR) {
w = MAX_TOTAL_VAR;
}

// sqrt((a + b * (sqrt((k - m)^2 + sigma^2) + rho * (k - m)))/tao)
return FixedPointMathLib.sqrt(uint(w).divideDecimal(uint(tao)));
// sqrt((a + b * (sqrt((k - m)^2 + sigma^2) + rho * (k - m)))/tau)
uint vol = FixedPointMathLib.sqrt(uint(w).divideDecimal(uint(tau)));
return vol;
}

/**
* @dev k = ln(strike / fwd), but being bounded by B: -B < k < B
* where B = TOTAL_VOL_SCALAR x sqrt(a + b * sig)
*/
function getK(uint strike, int a, uint b, uint sigma, uint forwardPrice) internal pure returns (int k) {
// calculate the bounds
int volFactor = int(FixedPointMathLib.sqrt((a + b.multiplyDecimal(sigma).toInt256()).toUint256()));
int k_bound = volFactor.multiplyDecimal(TOTAL_VOL_SCALAR);
int sk = int(strike.divideDecimal(forwardPrice));

if (sk == 0) return -k_bound;

// k = ln (strike / fwd)
k = FixedPointMathLib.ln(sk);

// make sure -B < k < B
if (k > k_bound) return k_bound;
if (k < -k_bound) return -k_bound;
}
}
93 changes: 86 additions & 7 deletions test/math/SVI.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "forge-std/console2.sol";
import "src/math/SVI.sol";

struct SVITestParams {
uint64 tao;
uint64 tau;
int a;
uint b;
int rho;
Expand All @@ -22,9 +22,13 @@ struct SVITestParams {
contract SVITester {
function getVol(uint strike, SVITestParams memory params) external pure returns (uint vol) {
uint res =
SVI.getVol(strike, params.a, params.b, params.rho, params.m, params.sigma, params.forwardPrice, params.tao);
SVI.getVol(strike, params.a, params.b, params.rho, params.m, params.sigma, params.forwardPrice, params.tau);
return res;
}

function getK(uint strike, SVITestParams memory params) external pure returns (int k) {
return SVI.getK(strike, params.a, params.b, params.sigma, params.forwardPrice);
}
}

contract SVITest is Test {
Expand All @@ -36,7 +40,7 @@ contract SVITest is Test {

function testGetVols() public {
SVITestParams memory params = SVITestParams({
tao: 0.00821917808219178e18,
tau: 0.00821917808219178e18,
a: 0.00821917808219178e18,
b: 0.01232876712328767e18,
rho: -int(0.000821917808219178e18),
Expand All @@ -62,7 +66,7 @@ contract SVITest is Test {
}

params = SVITestParams({
tao: 0.038356164383561646e18,
tau: 0.038356164383561646e18,
a: 0.027616438356164386e18,
b: 0.041424657534246574e18,
rho: 0.003452054794520548e18,
Expand All @@ -82,7 +86,7 @@ contract SVITest is Test {
}

params = SVITestParams({
tao: 0.2465753424657534e18,
tau: 0.2465753424657534e18,
a: 0.17753424657534247e18,
b: 0.17753424657534247e18,
rho: 0.05917808219178082e18,
Expand All @@ -104,15 +108,90 @@ contract SVITest is Test {

function testRevertsForBadParams() public {
SVITestParams memory params = SVITestParams({
tao: 0.00821917808219178e18,
tau: 0.00821917808219178e18,
a: -10e18,
b: 0.01232876712328767e18,
rho: -int(0.000821917808219178e18),
m: -int(0.000410958904109589e18),
sigma: 0.000410958904109589e18,
forwardPrice: 1800e18
});
vm.expectRevert(SVI.SVI_InvalidParameters.selector);
vm.expectRevert(bytes("SafeCast: value must be positive"));
tester.getVol(1800e18, params);
}

function testZeroStrikeVolIsZero() public {
uint forwardPrice = 2000e18;
SVITestParams memory params = _getDefaultSVIParams(forwardPrice);
uint vol = tester.getVol(0, params);
assertEq(vol, 0);
}

function testRevertWhenForwardPriceIsZero() public {
SVITestParams memory params = _getDefaultSVIParams(0);
vm.expectRevert(SVI.SVI_NoForwardPrice.selector);
tester.getVol(1800e18, params);
}

function testMaxKShouldBeCapped() public {
SVITestParams memory params = _getDefaultSVIParams(2000e18);
uint strike = 2000_000e18;

int k = tester.getK(strike, params);
assertEq(k / 1e12, 409878); // +0.409878

uint vol = tester.getVol(strike, params);
assertEq(vol / 1e12, 645048); // 0.645048
}

function testMinKShouldBeCapped() public {
SVITestParams memory params = _getDefaultSVIParams(2000e18);
uint strike = 1e18;

int k = tester.getK(strike, params);
assertEq(k / 1e12, -409878); // -0.409878

uint vol = tester.getVol(strike, params);
assertEq(vol / 1e12, 666465); // 0.666465
}

function testMaxVarCapped() public {
SVITestParams memory params = SVITestParams({
a: 2e18,
b: 0.6e18,
sigma: 0.3e18,
rho: -0.02e18,
m: 0.03e18,
tau: 0.03835616438e18, // 14/365
forwardPrice: 2000e18
});

uint strike = 200_000e18;
uint vol = tester.getVol(strike, params);

assertEq(vol / 1e12, 10212037); // vol is 10.21
}

function testFuzzGetVolCapped(uint strike, uint forwardPrice) public view {
vm.assume(forwardPrice != 0);
vm.assume(strike < 50_000_000e18);

SVITestParams memory params = _getDefaultSVIParams(forwardPrice);

uint vol = tester.getVol(strike, params);

assert(vol < 2e18);
}

function _getDefaultSVIParams(uint forwardPrice) internal pure returns (SVITestParams memory params) {
params = SVITestParams({
a: 0.01e18,
b: 0.01e18,
sigma: 0.05e18,
rho: -0.04e18,
m: 0.03e18,
tau: 0.03287671232e18, // 12/365
forwardPrice: forwardPrice
});
}
}

0 comments on commit 5c38c1b

Please sign in to comment.