|
|
""" |
|
|
Tests for options.py |
|
|
""" |
|
|
|
|
|
import datetime |
|
|
|
|
|
import pytest |
|
|
|
|
|
from src.folio.options import ( |
|
|
OptionContract, |
|
|
calculate_black_scholes_delta, |
|
|
calculate_bs_price, |
|
|
calculate_implied_volatility, |
|
|
parse_option_description, |
|
|
) |
|
|
|
|
|
|
|
|
def create_test_option( |
|
|
option_type="CALL", |
|
|
days_to_expiry=30, |
|
|
strike=100, |
|
|
underlying_price=100, |
|
|
): |
|
|
"""Create a test option position.""" |
|
|
expiry = datetime.datetime.now() + datetime.timedelta(days=days_to_expiry) |
|
|
return OptionContract( |
|
|
underlying="TEST", |
|
|
expiry=expiry, |
|
|
strike=strike, |
|
|
option_type=option_type, |
|
|
quantity=1, |
|
|
current_price=5.0, |
|
|
description=f"TEST {expiry.strftime('%b').upper()} {expiry.day} {expiry.year} ${strike} {option_type}", |
|
|
) |
|
|
|
|
|
|
|
|
def test_calculate_black_scholes_delta(): |
|
|
"""Test delta calculation.""" |
|
|
|
|
|
call_option = create_test_option(option_type="CALL", strike=100) |
|
|
delta = calculate_black_scholes_delta(call_option, 100, volatility=0.3) |
|
|
assert 0.45 < delta < 0.55 |
|
|
|
|
|
|
|
|
put_option = create_test_option(option_type="PUT", strike=100) |
|
|
delta = calculate_black_scholes_delta(put_option, 100, volatility=0.3) |
|
|
assert -0.55 < delta < -0.45 |
|
|
|
|
|
|
|
|
itm_call = create_test_option(option_type="CALL", strike=90) |
|
|
delta = calculate_black_scholes_delta(itm_call, 100, volatility=0.3) |
|
|
assert delta > 0.5 |
|
|
|
|
|
|
|
|
otm_call = create_test_option(option_type="CALL", strike=110) |
|
|
delta = calculate_black_scholes_delta(otm_call, 100, volatility=0.3) |
|
|
assert delta < 0.5 |
|
|
|
|
|
|
|
|
def test_calculate_bs_price(): |
|
|
"""Test price calculation.""" |
|
|
|
|
|
call_option = create_test_option(option_type="CALL", strike=100, days_to_expiry=30) |
|
|
price = calculate_bs_price(call_option, 100, volatility=0.3) |
|
|
assert 3 < price < 6 |
|
|
|
|
|
|
|
|
deep_itm_call = create_test_option(option_type="CALL", strike=80, days_to_expiry=30) |
|
|
price = calculate_bs_price(deep_itm_call, 100, volatility=0.3) |
|
|
assert 19 < price < 22 |
|
|
|
|
|
|
|
|
deep_otm_call = create_test_option( |
|
|
option_type="CALL", strike=150, days_to_expiry=30 |
|
|
) |
|
|
price = calculate_bs_price(deep_otm_call, 100, volatility=0.3) |
|
|
assert price < 1 |
|
|
|
|
|
|
|
|
def test_calculate_implied_volatility(): |
|
|
"""Test implied volatility calculation.""" |
|
|
|
|
|
option = create_test_option(option_type="CALL", strike=100, days_to_expiry=30) |
|
|
|
|
|
|
|
|
known_vol = 0.3 |
|
|
price = calculate_bs_price(option, 100, volatility=known_vol) |
|
|
|
|
|
|
|
|
implied_vol = calculate_implied_volatility(option, 100, price) |
|
|
|
|
|
|
|
|
assert abs(implied_vol - known_vol) < 0.01 |
|
|
|
|
|
|
|
|
def test_parse_option_description(): |
|
|
"""Test option description parsing.""" |
|
|
description = "AAPL JAN 15 2023 $150 CALL" |
|
|
result = parse_option_description(description) |
|
|
|
|
|
assert result["underlying"] == "AAPL" |
|
|
assert result["expiry"] == datetime.datetime(2023, 1, 15) |
|
|
assert result["strike"] == 150 |
|
|
assert result["option_type"] == "CALL" |
|
|
|
|
|
|
|
|
with pytest.raises(ValueError): |
|
|
parse_option_description("AAPL CALL") |
|
|
|
|
|
|
|
|
with pytest.raises(ValueError): |
|
|
parse_option_description("AAPL JAN 15 2023 150 CALL") |
|
|
|
|
|
|
|
|
with pytest.raises(ValueError): |
|
|
parse_option_description("AAPL FOO 15 2023 $150 CALL") |
|
|
|