44 lines
1.5 KiB
Python
44 lines
1.5 KiB
Python
import math
|
|
import pytest
|
|
from fxhnt.application.vrp_pricing import bs_put, implied_vol_put, put_delta, parity_forward
|
|
|
|
|
|
def test_bs_put_atm_positive_and_bounded():
|
|
# ATM put (F=K) has value between 0 and K; ~0.4*F*sigma*sqrt(tau) for small tau
|
|
p = bs_put(F=100.0, K=100.0, sigma=0.20, tau=30 / 365)
|
|
assert 0.0 < p < 100.0
|
|
assert p == pytest.approx(0.4 * 100.0 * 0.20 * math.sqrt(30 / 365), rel=0.05)
|
|
|
|
|
|
def test_bs_put_monotone_in_strike():
|
|
lo = bs_put(F=100.0, K=90.0, sigma=0.2, tau=0.1)
|
|
hi = bs_put(F=100.0, K=110.0, sigma=0.2, tau=0.1)
|
|
assert hi > lo # higher strike put is worth more
|
|
|
|
|
|
def test_implied_vol_round_trip():
|
|
price = bs_put(F=100.0, K=95.0, sigma=0.27, tau=45 / 365)
|
|
iv = implied_vol_put(price=price, F=100.0, K=95.0, tau=45 / 365)
|
|
assert iv == pytest.approx(0.27, abs=1e-4)
|
|
|
|
|
|
def test_implied_vol_returns_none_on_arbitrage():
|
|
# a price below intrinsic (K-F, discounted) has no arb-free IV
|
|
assert implied_vol_put(price=0.0001, F=100.0, K=130.0, tau=0.1) is None
|
|
|
|
|
|
def test_put_delta_otm_near_target():
|
|
# ~20-delta OTM put sits below spot; delta magnitude in (0,0.5)
|
|
d = put_delta(F=100.0, K=94.0, sigma=0.20, tau=30 / 365)
|
|
assert 0.0 < d < 0.5
|
|
|
|
|
|
def test_parity_forward_recovers_forward():
|
|
F = 100.0
|
|
tau = 0.1
|
|
sigma = 0.2
|
|
# build synthetic ATM call/put at K=100 consistent with forward F via parity: C - P = F - K
|
|
p = bs_put(F=F, K=100.0, sigma=sigma, tau=tau)
|
|
c = p + (F - 100.0)
|
|
assert parity_forward(call_atm=c, put_atm=p, k_atm=100.0) == pytest.approx(F, abs=1e-6)
|