From 59caab948801345169f869c591f8f7efc262958b Mon Sep 17 00:00:00 2001 From: Luca Mitas Date: Thu, 30 Apr 2026 14:33:09 -0300 Subject: [PATCH 1/2] FIX: Expose date, from, to params on options.quotes() Customer report: REST /v1/options/quotes/{symbol}/ accepts date, from, and to but the SDK's options.quotes() only exposed symbols, so historical date ranges were unreachable except via kwargs passthrough. Adds three optional fields to OptionsQuotesInput: - date -> wire param `date` - from_date -> wire param `from` (alias, since `from` is reserved in Python) - to_date -> wire param `to` (alias) The Pydantic alias pattern matches StocksCandlesInput / FundsCandlesInput / StocksEarningsInput. Adds a model_validator that rejects from_date > to_date, matching the same pattern. Tests: - from_date/to_date land on the wire as `from`/`to` (not snake_case) - date lands on the wire as `date` - from_date > to_date raises MinMaxDateValidationError Co-Authored-By: Claude Opus 4.7 (1M context) --- src/marketdata/input_types/options.py | 19 +++++++++++ src/tests/test_options_quotes.py | 48 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/marketdata/input_types/options.py b/src/marketdata/input_types/options.py index 220476d..12faf00 100644 --- a/src/marketdata/input_types/options.py +++ b/src/marketdata/input_types/options.py @@ -135,6 +135,20 @@ class OptionsQuotesInput(BaseInputType): symbols: str | list[str] = Field( description="A single symbol string or a list of symbol strings", min_length=1 ) + date: datetime.date | str | None = Field( + description="Historical end-of-day quote date (ISO 8601, unix, or spreadsheet)", + default=None, + ) + from_date: datetime.date | str | None = Field( + description="Start of date range, inclusive (ISO 8601, unix, or spreadsheet)", + default=None, + alias="from", + ) + to_date: datetime.date | str | None = Field( + description="End of date range, exclusive (ISO 8601, unix, or spreadsheet)", + default=None, + alias="to", + ) @field_validator("symbols") def validate_symbols(cls, value: str | list[str]) -> list[str]: @@ -142,6 +156,11 @@ def validate_symbols(cls, value: str | list[str]) -> list[str]: return value.split(",") if "," in value else [value] return value + @model_validator(mode="after") + def validate_input(self) -> "OptionsQuotesInput": + self._validate_min_max_dates("from_date", "to_date") + return self + class OptionsStrikesInput(BaseInputType): diff --git a/src/tests/test_options_quotes.py b/src/tests/test_options_quotes.py index 77959bf..3737c59 100644 --- a/src/tests/test_options_quotes.py +++ b/src/tests/test_options_quotes.py @@ -2,9 +2,12 @@ import pathlib from unittest.mock import patch +import pytest import pytz +from marketdata.exceptions import MinMaxDateValidationError from marketdata.input_types.base import OutputFormat +from marketdata.input_types.options import OptionsQuotesInput from marketdata.output_types.options_quotes import ( OptionsQuotes, OptionsQuotesHumanReadable, @@ -512,3 +515,48 @@ def test_options_quotes_human_readable_get_null_csv_string(): + ",".join([""] * len(fields)).replace("_", " ") ) assert null_csv_string == expected_csv_string + + +def test_options_quotes_input_date_range_aliases_on_wire(load_json, respx_mock, client): + mock_data = load_json("options_quotes_response_200") + respx_mock.get( + "https://api.marketdata.app/v1/options/quotes/AAPL271217C00255000/" + ).respond(json=mock_data, status_code=200) + + client.options.quotes( + symbols="AAPL271217C00255000", + from_date="2026-04-14", + to_date="2026-04-18", + output_format=OutputFormat.INTERNAL, + ) + + params = respx_mock.calls.last.request.url.params + assert params.get("from") == "2026-04-14" + assert params.get("to") == "2026-04-18" + assert params.get("from_date") is None + assert params.get("to_date") is None + + +def test_options_quotes_input_date_param_on_wire(load_json, respx_mock, client): + mock_data = load_json("options_quotes_response_200") + respx_mock.get( + "https://api.marketdata.app/v1/options/quotes/AAPL271217C00255000/" + ).respond(json=mock_data, status_code=200) + + client.options.quotes( + symbols="AAPL271217C00255000", + date="2026-04-15", + output_format=OutputFormat.INTERNAL, + ) + + params = respx_mock.calls.last.request.url.params + assert params.get("date") == "2026-04-15" + + +def test_options_quotes_input_from_after_to_raises(): + with pytest.raises(MinMaxDateValidationError): + OptionsQuotesInput( + symbols="AAPL271217C00255000", + from_date=datetime.date(2026, 4, 18), + to_date=datetime.date(2026, 4, 14), + ) From 68f8c6a9ac92a9cc594da80c83b3509d439382a4 Mon Sep 17 00:00:00 2001 From: Luca Mitas Date: Thu, 30 Apr 2026 14:33:20 -0300 Subject: [PATCH 2/2] FIX: Add missing from/to wire aliases on OptionsChainInput OptionsChainInput.from_date and to_date had no Pydantic alias, so they serialized as `from_date=...&to_date=...` instead of the wire's `from=...&to=...`. Every other date-range input in the SDK (StocksCandlesInput, StocksEarningsInput, StocksNewsInput, FundsCandlesInput, MarketsStatusInput) already uses alias="from" / alias="to" - chain was the outlier. Adds the aliases and a regression test asserting the rendered URL. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/marketdata/input_types/options.py | 8 ++++++-- src/tests/test_options_chain.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/marketdata/input_types/options.py b/src/marketdata/input_types/options.py index 12faf00..b3587a6 100644 --- a/src/marketdata/input_types/options.py +++ b/src/marketdata/input_types/options.py @@ -38,10 +38,14 @@ class OptionsChainInput(BaseInputType): description="The number of days to expiration to filter by", default=None ) from_date: datetime.date | str | None = Field( - description="The start date to fetch options chain for", default=None + description="The start date to fetch options chain for", + default=None, + alias="from", ) to_date: datetime.date | str | None = Field( - description="The end date to fetch options chain for", default=None + description="The end date to fetch options chain for", + default=None, + alias="to", ) month: int | None = Field(description="The month to filter by", default=None) year: int | None = Field(description="The year to filter by", default=None) diff --git a/src/tests/test_options_chain.py b/src/tests/test_options_chain.py index 2e1c0ce..acf464f 100644 --- a/src/tests/test_options_chain.py +++ b/src/tests/test_options_chain.py @@ -287,3 +287,23 @@ def test_get_options_chain_response_200_csv(respx_mock, client): "AAPL", output_format=OutputFormat.CSV, filename="test.csv" ) assert pathlib.Path(output).read_text() == "AS RECEIVED FROM API" + + +def test_options_chain_input_date_range_aliases_on_wire(load_json, respx_mock, client): + mock_data = load_json("options_chain_response_200") + respx_mock.get("https://api.marketdata.app/v1/options/chain/AAPL/").respond( + json=mock_data, status_code=200 + ) + + client.options.chain( + "AAPL", + from_date="2026-04-14", + to_date="2026-04-18", + output_format=OutputFormat.INTERNAL, + ) + + params = respx_mock.calls.last.request.url.params + assert params.get("from") == "2026-04-14" + assert params.get("to") == "2026-04-18" + assert params.get("from_date") is None + assert params.get("to_date") is None