folio / tests /test_options.py
dystomachina's picture
Initial commit for Folio project
ce4bc73
"""
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, # noqa: ARG001
):
"""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."""
# ATM call should have delta around 0.5
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
# ATM put should have delta around -0.5
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 should have delta > 0.5
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 should have 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."""
# ATM call with 30 days to expiry
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 # Reasonable range for ATM call
# Deep ITM call should be worth close to intrinsic value
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 # Intrinsic value is 20, plus some time value
# Deep OTM call should be worth very little
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 # Very little value
def test_calculate_implied_volatility():
"""Test implied volatility calculation."""
# Create an option with known parameters
option = create_test_option(option_type="CALL", strike=100, days_to_expiry=30)
# Calculate price with known volatility
known_vol = 0.3
price = calculate_bs_price(option, 100, volatility=known_vol)
# Calculate implied volatility from that price
implied_vol = calculate_implied_volatility(option, 100, price)
# Should recover the original volatility
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"
# Test invalid format
with pytest.raises(ValueError):
parse_option_description("AAPL CALL")
# Test invalid strike
with pytest.raises(ValueError):
parse_option_description("AAPL JAN 15 2023 150 CALL")
# Test invalid month
with pytest.raises(ValueError):
parse_option_description("AAPL FOO 15 2023 $150 CALL")