"""Integration tests for options tools."""
import pytest
import respx
from httpx import Response
from schwab_mcp.tools import options
# Sample API response
OPTION_CHAIN_RESPONSE = {
"symbol": "AAPL",
"status": "SUCCESS",
"isDelayed": False,
"numberOfContracts": 100,
"underlying": {
"last": 185.50,
"bid": 185.45,
"ask": 185.55,
"change": 2.50,
"percentChange": 1.37,
"totalVolume": 45000000,
},
"callExpDateMap": {
"2024-01-19:30": {
"180.0": [
{
"symbol": "AAPL240119C00180000",
"description": "AAPL Jan 19 2024 180 Call",
"expirationDate": "2024-01-19",
"daysToExpiration": 30,
"bid": 8.50,
"ask": 8.70,
"last": 8.60,
"mark": 8.60,
"totalVolume": 1500,
"openInterest": 5000,
"volatility": 0.25,
"delta": 0.65,
"gamma": 0.03,
"theta": -0.08,
"vega": 0.15,
"rho": 0.05,
"inTheMoney": True,
"intrinsicValue": 5.50,
"extrinsicValue": 3.10,
"timeValue": 3.10,
}
],
"185.0": [
{
"symbol": "AAPL240119C00185000",
"description": "AAPL Jan 19 2024 185 Call",
"expirationDate": "2024-01-19",
"daysToExpiration": 30,
"bid": 5.00,
"ask": 5.20,
"last": 5.10,
"mark": 5.10,
"totalVolume": 2500,
"openInterest": 8000,
"volatility": 0.24,
"delta": 0.50,
"gamma": 0.04,
"theta": -0.10,
"vega": 0.18,
"rho": 0.04,
"inTheMoney": True,
"intrinsicValue": 0.50,
"extrinsicValue": 4.60,
"timeValue": 4.60,
}
],
}
},
"putExpDateMap": {
"2024-01-19:30": {
"180.0": [
{
"symbol": "AAPL240119P00180000",
"description": "AAPL Jan 19 2024 180 Put",
"expirationDate": "2024-01-19",
"daysToExpiration": 30,
"bid": 2.80,
"ask": 3.00,
"last": 2.90,
"mark": 2.90,
"totalVolume": 1200,
"openInterest": 4000,
"volatility": 0.25,
"delta": -0.35,
"gamma": 0.03,
"theta": -0.06,
"vega": 0.15,
"rho": -0.03,
"inTheMoney": False,
"intrinsicValue": 0.00,
"extrinsicValue": 2.90,
"timeValue": 2.90,
}
],
}
},
}
class TestGetOptionChain:
"""Tests for get_option_chain tool."""
@respx.mock
@pytest.mark.asyncio
async def test_get_option_chain_all(self, mock_client):
"""Test getting full option chain (calls and puts)."""
respx.get("https://api.schwabapi.com/marketdata/v1/chains").mock(
return_value=Response(200, json=OPTION_CHAIN_RESPONSE)
)
result = await options.get_option_chain(
mock_client, {"symbol": "AAPL"}
)
assert result["symbol"] == "AAPL"
assert result["underlying_price"] == 185.50
assert result["status"] == "SUCCESS"
assert result["is_delayed"] is False
assert result["number_of_contracts"] == 100
# Check underlying data
assert result["underlying"]["last"] == 185.50
assert result["underlying"]["change"] == 2.50
# Check calls (should have 2)
assert len(result["calls"]) == 2
call_180 = next(c for c in result["calls"] if c["strike"] == 180.0)
assert call_180["symbol"] == "AAPL240119C00180000"
assert call_180["delta"] == 0.65
assert call_180["gamma"] == 0.03
assert call_180["theta"] == -0.08
assert call_180["vega"] == 0.15
assert call_180["in_the_money"] is True
# Check puts (should have 1)
assert len(result["puts"]) == 1
put_180 = result["puts"][0]
assert put_180["strike"] == 180.0
assert put_180["delta"] == -0.35
assert put_180["in_the_money"] is False
@respx.mock
@pytest.mark.asyncio
async def test_get_option_chain_calls_only(self, mock_client):
"""Test getting only call options."""
respx.get("https://api.schwabapi.com/marketdata/v1/chains").mock(
return_value=Response(200, json=OPTION_CHAIN_RESPONSE)
)
result = await options.get_option_chain(
mock_client, {"symbol": "AAPL", "contract_type": "CALL"}
)
assert len(result["calls"]) == 2
assert len(result["puts"]) == 0
@respx.mock
@pytest.mark.asyncio
async def test_get_option_chain_puts_only(self, mock_client):
"""Test getting only put options."""
respx.get("https://api.schwabapi.com/marketdata/v1/chains").mock(
return_value=Response(200, json=OPTION_CHAIN_RESPONSE)
)
result = await options.get_option_chain(
mock_client, {"symbol": "AAPL", "contract_type": "PUT"}
)
assert len(result["calls"]) == 0
assert len(result["puts"]) == 1
@respx.mock
@pytest.mark.asyncio
async def test_get_option_chain_with_dates(self, mock_client):
"""Test getting options with date filters."""
respx.get("https://api.schwabapi.com/marketdata/v1/chains").mock(
return_value=Response(200, json=OPTION_CHAIN_RESPONSE)
)
result = await options.get_option_chain(
mock_client,
{
"symbol": "AAPL",
"from_date": "2024-01-01",
"to_date": "2024-01-31",
},
)
assert result["symbol"] == "AAPL"
assert len(result["calls"]) == 2
@respx.mock
@pytest.mark.asyncio
async def test_get_option_chain_empty(self, mock_client):
"""Test handling empty option chain."""
empty_response = {
"symbol": "AAPL",
"status": "SUCCESS",
"underlying": {"last": 185.50},
"callExpDateMap": {},
"putExpDateMap": {},
}
respx.get("https://api.schwabapi.com/marketdata/v1/chains").mock(
return_value=Response(200, json=empty_response)
)
result = await options.get_option_chain(
mock_client, {"symbol": "AAPL"}
)
assert result["calls"] == []
assert result["puts"] == []
@respx.mock
@pytest.mark.asyncio
async def test_get_option_chain_sorted_by_strike(self, mock_client):
"""Test that options are sorted by expiration then strike."""
respx.get("https://api.schwabapi.com/marketdata/v1/chains").mock(
return_value=Response(200, json=OPTION_CHAIN_RESPONSE)
)
result = await options.get_option_chain(
mock_client, {"symbol": "AAPL"}
)
# Calls should be sorted: 180, 185
strikes = [c["strike"] for c in result["calls"]]
assert strikes == [180.0, 185.0]