From 6fc33411beb5e86a099639c3b87f5f61ef87eb6f Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Mon, 9 Jun 2025 00:26:49 -0700 Subject: [PATCH 01/12] Add futures beta support --- polygon/rest/__init__.py | 2 + polygon/rest/futures.py | 338 ++++++++++++++++++++++++++++++++ polygon/rest/models/__init__.py | 1 + polygon/rest/models/futures.py | 338 ++++++++++++++++++++++++++++++++ 4 files changed, 679 insertions(+) create mode 100644 polygon/rest/futures.py create mode 100644 polygon/rest/models/futures.py diff --git a/polygon/rest/__init__.py b/polygon/rest/__init__.py index 7484378e..ed57ee72 100644 --- a/polygon/rest/__init__.py +++ b/polygon/rest/__init__.py @@ -1,4 +1,5 @@ from .aggs import AggsClient +from .futures import FuturesClient from .trades import TradesClient from .quotes import QuotesClient from .snapshot import SnapshotClient @@ -23,6 +24,7 @@ class RESTClient( AggsClient, + FuturesClient, TradesClient, QuotesClient, SnapshotClient, diff --git a/polygon/rest/futures.py b/polygon/rest/futures.py new file mode 100644 index 00000000..eb396a48 --- /dev/null +++ b/polygon/rest/futures.py @@ -0,0 +1,338 @@ +from typing import Optional, Any, Dict, List, Union, Iterator +from urllib3 import HTTPResponse +from datetime import datetime, date + +from .base import BaseClient +from .models.futures import ( + FuturesAgg, + FuturesContract, + FuturesProduct, + FuturesQuote, + FuturesTrade, + FuturesSchedule, +) +from .models.common import Sort, Order +from .models.request import RequestOptionBuilder + + +class FuturesClient(BaseClient): + """ + Client for the Futures REST Endpoints + (aligned with the paths from /futures/vX/...) + """ + + def list_futures_aggregates( + self, + ticker: str, + resolution: str, + window_start: Optional[str] = None, + window_start_lt: Optional[str] = None, + window_start_lte: Optional[str] = None, + window_start_gt: Optional[str] = None, + window_start_gte: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[Union[str, Order]] = None, + sort: Optional[Union[str, Sort]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[Iterator[FuturesAgg], HTTPResponse]: + """ + Endpoint: GET /futures/vX/aggs/{ticker} + + Get aggregates for a futures contract in a given time range. + This endpoint returns data that includes: + - open, close, high, low + - volume, dollar_volume, etc. + If `next_url` is present, it will be paginated. + """ + url = f"/futures/vX/aggs/{ticker}" + return self._paginate( + path=url, + params=self._get_params(self.list_aggregates, locals()), + raw=raw, + deserializer=FuturesAgg.from_dict, + options=options, + ) + + def list_futures_contracts( + self, + product_code: Optional[str] = None, + first_trade_date: Optional[Union[str, date]] = None, + last_trade_date: Optional[Union[str, date]] = None, + as_of: Optional[Union[str, date]] = None, + active: Optional[str] = None, + type: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[Union[str, Order]] = None, + sort: Optional[Union[str, Sort]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[Iterator[FuturesContract], HTTPResponse]: + """ + Endpoint: GET /futures/vX/contracts + + The Contracts endpoint returns a paginated list of futures contracts. + """ + url = "/futures/vX/contracts" + return self._paginate( + path=url, + params=self._get_params(self.list_contracts, locals()), + raw=raw, + deserializer=FuturesContract.from_dict, + options=options, + ) + + def get_futures_contract_details( + self, + ticker: str, + as_of: Optional[Union[str, date]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[FuturesContract, HTTPResponse]: + """ + Endpoint: GET /futures/vX/contracts/{ticker} + + Returns details for a single contract at a specified point in time. + (No next_url in the response -> just a single get). + """ + url = f"/futures/vX/contracts/{ticker}" + return self._get( + path=url, + params=self._get_params(self.get_contract_details, locals()), + deserializer=FuturesContract.from_dict, + raw=raw, + result_key="results", + options=options, + ) + + def list_futures_products( + self, + name: Optional[str] = None, + name_search: Optional[str] = None, + as_of: Optional[Union[str, date]] = None, + market_identifier_code: Optional[str] = None, + sector: Optional[str] = None, + sub_sector: Optional[str] = None, + asset_class: Optional[str] = None, + asset_sub_class: Optional[str] = None, + type: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[Union[str, Order]] = None, + sort: Optional[Union[str, Sort]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[Iterator[FuturesProduct], HTTPResponse]: + """ + Endpoint: GET /futures/vX/products + + Returns a list of futures products (including combos). + """ + url = "/futures/vX/products" + return self._paginate( + path=url, + params=self._get_params(self.list_products, locals()), + raw=raw, + deserializer=FuturesProduct.from_dict, + options=options, + ) + + def get_futures_product_details( + self, + product_code: str, + type: Optional[str] = None, + as_of: Optional[Union[str, date]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[FuturesProduct, HTTPResponse]: + """ + Endpoint: GET /futures/vX/products/{product_code} + + Returns the details for a single product as it was at a specific day. + (No next_url -> single get). + """ + url = f"/futures/vX/products/{product_code}" + return self._get( + path=url, + params=self._get_params(self.get_product_details, locals()), + deserializer=FuturesProduct.from_dict, + raw=raw, + result_key="results", + options=options, + ) + + def list_futures_quotes( + self, + ticker: str, + timestamp: Optional[str] = None, + timestamp_lt: Optional[str] = None, + timestamp_lte: Optional[str] = None, + timestamp_gt: Optional[str] = None, + timestamp_gte: Optional[str] = None, + session_end_date: Optional[str] = None, + session_end_date_lt: Optional[str] = None, + session_end_date_lte: Optional[str] = None, + session_end_date_gt: Optional[str] = None, + session_end_date_gte: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[Union[str, Order]] = None, + sort: Optional[Union[str, Sort]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[Iterator[FuturesQuote], HTTPResponse]: + """ + Endpoint: GET /futures/vX/quotes/{ticker} + + Get quotes for a contract in a given time range (paginated). + """ + url = f"/futures/vX/quotes/{ticker}" + return self._paginate( + path=url, + params=self._get_params(self.list_quotes, locals()), + raw=raw, + deserializer=FuturesQuote.from_dict, + options=options, + ) + + def list_futures_trades( + self, + ticker: str, + timestamp: Optional[str] = None, + timestamp_lt: Optional[str] = None, + timestamp_lte: Optional[str] = None, + timestamp_gt: Optional[str] = None, + timestamp_gte: Optional[str] = None, + session_end_date: Optional[str] = None, + session_end_date_lt: Optional[str] = None, + session_end_date_lte: Optional[str] = None, + session_end_date_gt: Optional[str] = None, + session_end_date_gte: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[Union[str, Order]] = None, + sort: Optional[Union[str, Sort]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[Iterator[FuturesTrade], HTTPResponse]: + """ + Endpoint: GET /futures/vX/trades/{ticker} + + Get trades for a contract in a given time range (paginated). + """ + url = f"/futures/vX/trades/{ticker}" + return self._paginate( + path=url, + params=self._get_params(self.list_trades, locals()), + raw=raw, + deserializer=FuturesTrade.from_dict, + options=options, + ) + + def list_futures_schedules( + self, + session_end_date: Optional[str] = None, + market_identifier_code: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[Union[str, Order]] = None, + sort: Optional[Union[str, Sort]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[Iterator[FuturesSchedule], HTTPResponse]: + """ + Endpoint: GET /futures/vX/schedules + + Returns a list of trading schedules for multiple futures products on a specific date. + If `next_url` is present, this is paginated. + """ + url = "/futures/vX/schedules" + return self._paginate( + path=url, + params=self._get_params(self.list_schedules, locals()), + raw=raw, + deserializer=FuturesSchedule.from_dict, + options=options, + ) + + def list_futures_schedules_by_product_code( + self, + product_code: str, + session_end_date: Optional[str] = None, + session_end_date_lt: Optional[str] = None, + session_end_date_lte: Optional[str] = None, + session_end_date_gt: Optional[str] = None, + session_end_date_gte: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[Union[str, Order]] = None, + sort: Optional[Union[str, Sort]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[Iterator[FuturesSchedule], HTTPResponse]: + """ + Endpoint: GET /futures/vX/schedules/{product_code} + + Returns schedule data for a single product across (potentially) many trading dates. + """ + url = f"/futures/vX/schedules/{product_code}" + return self._paginate( + path=url, + params=self._get_params(self.list_schedules_by_product_code, locals()), + raw=raw, + deserializer=FuturesSchedule.from_dict, + options=options, + ) + + def list_futures_market_statuses( + self, + product_code_any_of: Optional[str] = None, + product_code: Optional[str] = None, + limit: Optional[int] = None, + order: Optional[Union[str, Order]] = None, + sort: Optional[Union[str, Sort]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[Iterator[FuturesMarketStatus], HTTPResponse]: + url = "/futures/vX/market-status" + return self._paginate( + path=url, + params=self._get_params(self.list_market_statuses, locals()), + raw=raw, + deserializer=FuturesMarketStatus.from_dict, + options=options, + ) + + def get_futures_snapshot( + self, + ticker: Optional[str] = None, + ticker_any_of: Optional[str] = None, + ticker_gt: Optional[str] = None, + ticker_gte: Optional[str] = None, + ticker_lt: Optional[str] = None, + ticker_lte: Optional[str] = None, + product_code: Optional[str] = None, + product_code_any_of: Optional[str] = None, + product_code_gt: Optional[str] = None, + product_code_gte: Optional[str] = None, + product_code_lt: Optional[str] = None, + product_code_lte: Optional[str] = None, + limit: Optional[int] = None, + sort: Optional[Union[str, Sort]] = None, + params: Optional[Dict[str, Any]] = None, + raw: bool = False, + options: Optional[RequestOptionBuilder] = None, + ) -> Union[Iterator[FuturesSnapshot], HTTPResponse]: + url = "/futures/vX/snapshot" + return self._paginate( + path=url, + params=self._get_params(self.get_snapshot, locals()), + raw=raw, + deserializer=FuturesSnapshot.from_dict, + options=options, + ) diff --git a/polygon/rest/models/__init__.py b/polygon/rest/models/__init__.py index 2c9a8086..3108ab01 100644 --- a/polygon/rest/models/__init__.py +++ b/polygon/rest/models/__init__.py @@ -5,6 +5,7 @@ from .dividends import * from .exchanges import * from .financials import * +from .futures import * from .indicators import * from .markets import * from .quotes import * diff --git a/polygon/rest/models/futures.py b/polygon/rest/models/futures.py new file mode 100644 index 00000000..3a3a7601 --- /dev/null +++ b/polygon/rest/models/futures.py @@ -0,0 +1,338 @@ +from typing import Optional, List +from ...modelclass import modelclass + + +@modelclass +class FuturesAgg: + """ + A single aggregate bar for a futures contract in a given time window. + Corresponds to /futures/vX/aggs/{ticker}. + """ + + ticker: Optional[str] = None + underlying_asset: Optional[str] = None + open: Optional[float] = None + high: Optional[float] = None + low: Optional[float] = None + close: Optional[float] = None + volume: Optional[float] = None + dollar_volume: Optional[float] = None + transaction_count: Optional[int] = None + window_start: Optional[int] = None + session_end_date: Optional[str] = None + settlement_price: Optional[float] = None + + @staticmethod + def from_dict(d): + return FuturesAgg( + ticker=d.get("ticker"), + underlying_asset=d.get("underlying_asset"), + open=d.get("open"), + high=d.get("high"), + low=d.get("low"), + close=d.get("close"), + volume=d.get("volume"), + dollar_volume=d.get("dollar_volume"), + transaction_count=d.get("transaction_count"), + window_start=d.get("window_start"), + session_end_date=d.get("session_end_date"), + settlement_price=d.get("settlement_price"), + ) + + +@modelclass +class FuturesContract: + """ + Represents a single futures contract (or a 'combo' contract). + Corresponds to /futures/vX/contracts endpoints. + """ + + ticker: Optional[str] = None + product_code: Optional[str] = None + market_identifier_code: Optional[str] = None + name: Optional[str] = None + type: Optional[str] = None + as_of: Optional[str] = None + active: Optional[bool] = None + first_trade_date: Optional[str] = None + last_trade_date: Optional[str] = None + days_to_maturity: Optional[int] = None + min_order_quantity: Optional[int] = None + max_order_quantity: Optional[int] = None + settlement_date: Optional[str] = None + settlement_tick_size: Optional[float] = None + spread_tick_size: Optional[float] = None + trade_tick_size: Optional[float] = None + maturity: Optional[str] = None + + @staticmethod + def from_dict(d): + return FuturesContract( + ticker=d.get("ticker"), + product_code=d.get("product_code"), + market_identifier_code=d.get("market_identifier_code"), + name=d.get("name"), + type=d.get("type"), + as_of=d.get("as_of"), + active=d.get("active"), + first_trade_date=d.get("first_trade_date"), + last_trade_date=d.get("last_trade_date"), + days_to_maturity=d.get("days_to_maturity"), + min_order_quantity=d.get("min_order_quantity"), + max_order_quantity=d.get("max_order_quantity"), + settlement_date=d.get("settlement_date"), + settlement_tick_size=d.get("settlement_tick_size"), + spread_tick_size=d.get("spread_tick_size"), + trade_tick_size=d.get("trade_tick_size"), + maturity=d.get("maturity"), + ) + + +@modelclass +class FuturesProduct: + """ + Represents a single futures product (or product 'combo'). + Corresponds to /futures/vX/products endpoints. + """ + + product_code: Optional[str] = None + name: Optional[str] = None + as_of: Optional[str] = None + market_identifier_code: Optional[str] = None + asset_class: Optional[str] = None + asset_sub_class: Optional[str] = None + sector: Optional[str] = None + sub_sector: Optional[str] = None + type: Optional[str] = None + last_updated: Optional[str] = None + otc_eligible: Optional[bool] = None + price_quotation: Optional[str] = None + settlement_currency_code: Optional[str] = None + settlement_method: Optional[str] = None + settlement_type: Optional[str] = None + trade_currency_code: Optional[str] = None + unit_of_measure: Optional[str] = None + unit_of_measure_quantity: Optional[float] = None + + @staticmethod + def from_dict(d): + return FuturesProduct( + product_code=d.get("product_code"), + name=d.get("name"), + as_of=d.get("as_of"), + market_identifier_code=d.get("market_identifier_code"), + asset_class=d.get("asset_class"), + asset_sub_class=d.get("asset_sub_class"), + sector=d.get("sector"), + sub_sector=d.get("sub_sector"), + type=d.get("type"), + last_updated=d.get("last_updated"), + otc_eligible=d.get("otc_eligible"), + price_quotation=d.get("price_quotation"), + settlement_currency_code=d.get("settlement_currency_code"), + settlement_method=d.get("settlement_method"), + settlement_type=d.get("settlement_type"), + trade_currency_code=d.get("trade_currency_code"), + unit_of_measure=d.get("unit_of_measure"), + unit_of_measure_quantity=d.get("unit_of_measure_quantity"), + ) + + +@modelclass +class FuturesQuote: + """ + Represents a futures NBBO quote within a given time range. + Corresponds to /futures/vX/quotes/{ticker} + """ + + ticker: Optional[str] = None + timestamp: Optional[int] = None + session_end_date: Optional[str] = None + ask_price: Optional[float] = None + ask_size: Optional[float] = None + ask_timestamp: Optional[int] = None + bid_price: Optional[float] = None + bid_size: Optional[float] = None + bid_timestamp: Optional[int] = None + + @staticmethod + def from_dict(d): + return FuturesQuote( + ticker=d.get("ticker"), + timestamp=d.get("timestamp"), + session_end_date=d.get("session_end_date"), + ask_price=d.get("ask_price"), + ask_size=d.get("ask_size"), + ask_timestamp=d.get("ask_timestamp"), + bid_price=d.get("bid_price"), + bid_size=d.get("bid_size"), + bid_timestamp=d.get("bid_timestamp"), + ) + + +@modelclass +class FuturesTrade: + """ + Represents a futures trade within a given time range. + Corresponds to /futures/vX/trades/{ticker} + """ + + ticker: Optional[str] = None + timestamp: Optional[int] = None + session_end_date: Optional[str] = None + price: Optional[float] = None + size: Optional[float] = None + + @staticmethod + def from_dict(d): + return FuturesTrade( + ticker=d.get("ticker"), + timestamp=d.get("timestamp"), + session_end_date=d.get("session_end_date"), + price=d.get("price"), + size=d.get("size"), + ) + + +@modelclass +class FuturesScheduleEvent: + """ + Represents a single market event for a schedule (preopen, open, closed, etc.). + """ + + event: Optional[str] = None + timestamp: Optional[str] = None + + @staticmethod + def from_dict(d): + return FuturesScheduleEvent( + event=d.get("event"), + timestamp=d.get("timestamp"), + ) + + +@modelclass +class FuturesSchedule: + """ + Represents a single schedule for a given session_end_date, with events. + Corresponds to /futures/vX/schedules, /futures/vX/schedules/{product_code} + """ + + session_end_date: Optional[str] = None + product_code: Optional[str] = None + market_identifier_code: Optional[str] = None + product_name: Optional[str] = None + schedule: Optional[List[FuturesScheduleEvent]] = None + + @staticmethod + def from_dict(d): + return FuturesSchedule( + session_end_date=d.get("session_end_date"), + product_code=d.get("product_code"), + market_identifier_code=d.get("market_identifier_code"), + product_name=d.get("product_name"), + schedule=[ + FuturesScheduleEvent.from_dict(ev) for ev in d.get("schedule", []) + ], + ) + + +@modelclass +class FuturesMarketStatus: + market_identifier_code: Optional[str] = None + market_status: Optional[str] = ( + None # Enum: pre_open, open, close, pause, post_close_pre_open + ) + product_code: Optional[str] = None + + @staticmethod + def from_dict(d): + return FuturesMarketStatus( + market_identifier_code=d.get("market_identifier_code"), + market_status=d.get("market_status"), + product_code=d.get("product_code"), + ) + + @modelclass + class FuturesSnapshotDetails: + open_interest: Optional[int] = None + settlement_date: Optional[int] = None + + @modelclass + class FuturesSnapshotMinute: + close: Optional[float] = None + high: Optional[float] = None + last_updated: Optional[int] = None + low: Optional[float] = None + open: Optional[float] = None + volume: Optional[float] = None + + @modelclass + class FuturesSnapshotQuote: + ask: Optional[float] = None + ask_size: Optional[int] = None + ask_timestamp: Optional[int] = None + bid: Optional[float] = None + bid_size: Optional[int] = None + bid_timestamp: Optional[int] = None + last_updated: Optional[int] = None + + @modelclass + class FuturesSnapshotTrade: + last_updated: Optional[int] = None + price: Optional[float] = None + size: Optional[int] = None + + @modelclass + class FuturesSnapshotSession: + change: Optional[float] = None + change_percent: Optional[float] = None + close: Optional[float] = None + high: Optional[float] = None + low: Optional[float] = None + open: Optional[float] = None + previous_settlement: Optional[float] = None + settlement_price: Optional[float] = None + volume: Optional[float] = None + + @modelclass + class FuturesSnapshot: + ticker: Optional[str] = None + product_code: Optional[str] = None + details: Optional[FuturesSnapshotDetails] = None + last_minute: Optional[FuturesSnapshotMinute] = None + last_quote: Optional[FuturesSnapshotQuote] = None + last_trade: Optional[FuturesSnapshotTrade] = None + session: Optional[FuturesSnapshotSession] = None + + @staticmethod + def from_dict(d): + return FuturesSnapshot( + ticker=d.get("ticker"), + product_code=d.get("product_code"), + details=( + FuturesSnapshotDetails.from_dict(d.get("details", {})) + if d.get("details") + else None + ), + last_minute=( + FuturesSnapshotMinute.from_dict(d.get("last_minute", {})) + if d.get("last_minute") + else None + ), + last_quote=( + FuturesSnapshotQuote.from_dict(d.get("last_quote", {})) + if d.get("last_quote") + else None + ), + last_trade=( + FuturesSnapshotTrade.from_dict(d.get("last_trade", {})) + if d.get("last_trade") + else None + ), + session=( + FuturesSnapshotSession.from_dict(d.get("session", {})) + if d.get("session") + else None + ), + ) From f0f0465749a5409c73613b5e30d03b10bad4ac58 Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Mon, 9 Jun 2025 00:34:43 -0700 Subject: [PATCH 02/12] Fix lint --- polygon/rest/futures.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/polygon/rest/futures.py b/polygon/rest/futures.py index eb396a48..914d90b2 100644 --- a/polygon/rest/futures.py +++ b/polygon/rest/futures.py @@ -10,6 +10,8 @@ FuturesQuote, FuturesTrade, FuturesSchedule, + FuturesMarketStatus, + FuturesSnapshot, ) from .models.common import Sort, Order from .models.request import RequestOptionBuilder @@ -49,7 +51,7 @@ def list_futures_aggregates( url = f"/futures/vX/aggs/{ticker}" return self._paginate( path=url, - params=self._get_params(self.list_aggregates, locals()), + params=self._get_params(self.list_futures_aggregates, locals()), raw=raw, deserializer=FuturesAgg.from_dict, options=options, @@ -78,7 +80,7 @@ def list_futures_contracts( url = "/futures/vX/contracts" return self._paginate( path=url, - params=self._get_params(self.list_contracts, locals()), + params=self._get_params(self.list_futures_contracts, locals()), raw=raw, deserializer=FuturesContract.from_dict, options=options, @@ -101,7 +103,7 @@ def get_futures_contract_details( url = f"/futures/vX/contracts/{ticker}" return self._get( path=url, - params=self._get_params(self.get_contract_details, locals()), + params=self._get_params(self.get_futures_contract_details, locals()), deserializer=FuturesContract.from_dict, raw=raw, result_key="results", @@ -134,7 +136,7 @@ def list_futures_products( url = "/futures/vX/products" return self._paginate( path=url, - params=self._get_params(self.list_products, locals()), + params=self._get_params(self.list_futures_products, locals()), raw=raw, deserializer=FuturesProduct.from_dict, options=options, @@ -158,7 +160,7 @@ def get_futures_product_details( url = f"/futures/vX/products/{product_code}" return self._get( path=url, - params=self._get_params(self.get_product_details, locals()), + params=self._get_params(self.get_futures_product_details, locals()), deserializer=FuturesProduct.from_dict, raw=raw, result_key="results", @@ -193,7 +195,7 @@ def list_futures_quotes( url = f"/futures/vX/quotes/{ticker}" return self._paginate( path=url, - params=self._get_params(self.list_quotes, locals()), + params=self._get_params(self.list_futures_quotes, locals()), raw=raw, deserializer=FuturesQuote.from_dict, options=options, @@ -227,7 +229,7 @@ def list_futures_trades( url = f"/futures/vX/trades/{ticker}" return self._paginate( path=url, - params=self._get_params(self.list_trades, locals()), + params=self._get_params(self.list_futures_trades, locals()), raw=raw, deserializer=FuturesTrade.from_dict, options=options, @@ -253,7 +255,7 @@ def list_futures_schedules( url = "/futures/vX/schedules" return self._paginate( path=url, - params=self._get_params(self.list_schedules, locals()), + params=self._get_params(self.list_futures_schedules, locals()), raw=raw, deserializer=FuturesSchedule.from_dict, options=options, @@ -282,7 +284,7 @@ def list_futures_schedules_by_product_code( url = f"/futures/vX/schedules/{product_code}" return self._paginate( path=url, - params=self._get_params(self.list_schedules_by_product_code, locals()), + params=self._get_params(self.list_futures_schedules_by_product_code, locals()), raw=raw, deserializer=FuturesSchedule.from_dict, options=options, @@ -302,7 +304,7 @@ def list_futures_market_statuses( url = "/futures/vX/market-status" return self._paginate( path=url, - params=self._get_params(self.list_market_statuses, locals()), + params=self._get_params(self.list_futures_market_statuses, locals()), raw=raw, deserializer=FuturesMarketStatus.from_dict, options=options, @@ -331,7 +333,7 @@ def get_futures_snapshot( url = "/futures/vX/snapshot" return self._paginate( path=url, - params=self._get_params(self.get_snapshot, locals()), + params=self._get_params(self.get_futures_snapshot, locals()), raw=raw, deserializer=FuturesSnapshot.from_dict, options=options, From 01e0d0413ac05a68aac230534f8544ee40d1bfdf Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Mon, 9 Jun 2025 00:38:57 -0700 Subject: [PATCH 03/12] Fix indent --- polygon/rest/futures.py | 4 +- polygon/rest/models/futures.py | 172 +++++++++++++++++---------------- 2 files changed, 92 insertions(+), 84 deletions(-) diff --git a/polygon/rest/futures.py b/polygon/rest/futures.py index 914d90b2..8d765557 100644 --- a/polygon/rest/futures.py +++ b/polygon/rest/futures.py @@ -284,7 +284,9 @@ def list_futures_schedules_by_product_code( url = f"/futures/vX/schedules/{product_code}" return self._paginate( path=url, - params=self._get_params(self.list_futures_schedules_by_product_code, locals()), + params=self._get_params( + self.list_futures_schedules_by_product_code, locals() + ), raw=raw, deserializer=FuturesSchedule.from_dict, options=options, diff --git a/polygon/rest/models/futures.py b/polygon/rest/models/futures.py index 3a3a7601..8f6263e2 100644 --- a/polygon/rest/models/futures.py +++ b/polygon/rest/models/futures.py @@ -253,86 +253,92 @@ def from_dict(d): product_code=d.get("product_code"), ) - @modelclass - class FuturesSnapshotDetails: - open_interest: Optional[int] = None - settlement_date: Optional[int] = None - - @modelclass - class FuturesSnapshotMinute: - close: Optional[float] = None - high: Optional[float] = None - last_updated: Optional[int] = None - low: Optional[float] = None - open: Optional[float] = None - volume: Optional[float] = None - - @modelclass - class FuturesSnapshotQuote: - ask: Optional[float] = None - ask_size: Optional[int] = None - ask_timestamp: Optional[int] = None - bid: Optional[float] = None - bid_size: Optional[int] = None - bid_timestamp: Optional[int] = None - last_updated: Optional[int] = None - - @modelclass - class FuturesSnapshotTrade: - last_updated: Optional[int] = None - price: Optional[float] = None - size: Optional[int] = None - - @modelclass - class FuturesSnapshotSession: - change: Optional[float] = None - change_percent: Optional[float] = None - close: Optional[float] = None - high: Optional[float] = None - low: Optional[float] = None - open: Optional[float] = None - previous_settlement: Optional[float] = None - settlement_price: Optional[float] = None - volume: Optional[float] = None - - @modelclass - class FuturesSnapshot: - ticker: Optional[str] = None - product_code: Optional[str] = None - details: Optional[FuturesSnapshotDetails] = None - last_minute: Optional[FuturesSnapshotMinute] = None - last_quote: Optional[FuturesSnapshotQuote] = None - last_trade: Optional[FuturesSnapshotTrade] = None - session: Optional[FuturesSnapshotSession] = None - - @staticmethod - def from_dict(d): - return FuturesSnapshot( - ticker=d.get("ticker"), - product_code=d.get("product_code"), - details=( - FuturesSnapshotDetails.from_dict(d.get("details", {})) - if d.get("details") - else None - ), - last_minute=( - FuturesSnapshotMinute.from_dict(d.get("last_minute", {})) - if d.get("last_minute") - else None - ), - last_quote=( - FuturesSnapshotQuote.from_dict(d.get("last_quote", {})) - if d.get("last_quote") - else None - ), - last_trade=( - FuturesSnapshotTrade.from_dict(d.get("last_trade", {})) - if d.get("last_trade") - else None - ), - session=( - FuturesSnapshotSession.from_dict(d.get("session", {})) - if d.get("session") - else None - ), - ) + +@modelclass +class FuturesSnapshotDetails: + open_interest: Optional[int] = None + settlement_date: Optional[int] = None + + +@modelclass +class FuturesSnapshotMinute: + close: Optional[float] = None + high: Optional[float] = None + last_updated: Optional[int] = None + low: Optional[float] = None + open: Optional[float] = None + volume: Optional[float] = None + + +@modelclass +class FuturesSnapshotQuote: + ask: Optional[float] = None + ask_size: Optional[int] = None + ask_timestamp: Optional[int] = None + bid: Optional[float] = None + bid_size: Optional[int] = None + bid_timestamp: Optional[int] = None + last_updated: Optional[int] = None + + +@modelclass +class FuturesSnapshotTrade: + last_updated: Optional[int] = None + price: Optional[float] = None + size: Optional[int] = None + + +@modelclass +class FuturesSnapshotSession: + change: Optional[float] = None + change_percent: Optional[float] = None + close: Optional[float] = None + high: Optional[float] = None + low: Optional[float] = None + open: Optional[float] = None + previous_settlement: Optional[float] = None + settlement_price: Optional[float] = None + volume: Optional[float] = None + + +@modelclass +class FuturesSnapshot: + ticker: Optional[str] = None + product_code: Optional[str] = None + details: Optional[FuturesSnapshotDetails] = None + last_minute: Optional[FuturesSnapshotMinute] = None + last_quote: Optional[FuturesSnapshotQuote] = None + last_trade: Optional[FuturesSnapshotTrade] = None + session: Optional[FuturesSnapshotSession] = None + + @staticmethod + def from_dict(d): + return FuturesSnapshot( + ticker=d.get("ticker"), + product_code=d.get("product_code"), + details=( + FuturesSnapshotDetails.from_dict(d.get("details", {})) + if d.get("details") + else None + ), + last_minute=( + FuturesSnapshotMinute.from_dict(d.get("last_minute", {})) + if d.get("last_minute") + else None + ), + last_quote=( + FuturesSnapshotQuote.from_dict(d.get("last_quote", {})) + if d.get("last_quote") + else None + ), + last_trade=( + FuturesSnapshotTrade.from_dict(d.get("last_trade", {})) + if d.get("last_trade") + else None + ), + session=( + FuturesSnapshotSession.from_dict(d.get("session", {})) + if d.get("session") + else None + ), + ) From 430b368b8591fb058be2705582362af6fb27a8e5 Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Mon, 9 Jun 2025 01:42:49 -0700 Subject: [PATCH 04/12] Add websocket support --- polygon/websocket/__init__.py | 2 +- polygon/websocket/models/__init__.py | 95 ++++++++++++++++++---------- polygon/websocket/models/common.py | 12 ++-- polygon/websocket/models/models.py | 90 ++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 43 deletions(-) diff --git a/polygon/websocket/__init__.py b/polygon/websocket/__init__.py index 77865d3f..9c4d4e4b 100644 --- a/polygon/websocket/__init__.py +++ b/polygon/websocket/__init__.py @@ -140,7 +140,7 @@ async def connect( if m["ev"] == "status": logger.debug("status: %s", m["message"]) continue - cmsg = parse(msgJson, logger) + cmsg = parse(msgJson, logger, self.market) if len(cmsg) > 0: await processor(cmsg) # type: ignore diff --git a/polygon/websocket/models/__init__.py b/polygon/websocket/models/__init__.py index 06cab55d..88d0829a 100644 --- a/polygon/websocket/models/__init__.py +++ b/polygon/websocket/models/__init__.py @@ -3,47 +3,72 @@ from .models import * import logging +# Define the mapping of market and event type to model class +MARKET_EVENT_MAP = { + Market.Stocks: { + "A": EquityAgg, + "AM": EquityAgg, + "T": EquityTrade, + "Q": EquityQuote, + "LULD": LimitUpLimitDown, + "FMV": FairMarketValue, + "NOI": Imbalance, + "LV": LaunchpadValue, + }, + Market.Options: { + "A": EquityAgg, + "AM": EquityAgg, + "T": EquityTrade, + "Q": EquityQuote, + "FMV": FairMarketValue, + }, + Market.Indices: { + "A": EquityAgg, + "AM": EquityAgg, + "V": IndexValue, + }, + Market.Futures: { + "A": FuturesAgg, + "AM": FuturesAgg, + "T": FuturesTrade, + "Q": FuturesQuote, + }, + Market.Crypto: { + "XA": CurrencyAgg, + "XAS": CurrencyAgg, + "XT": CryptoTrade, + "XQ": CryptoQuote, + "XL2": Level2Book, + "FMV": FairMarketValue, + }, + Market.Forex: { + "CA": CurrencyAgg, + "CAS": CurrencyAgg, + "C": ForexQuote, + "FMV": FairMarketValue, + }, +} -def parse_single(data: Dict[str, Any]): + +def parse_single(data: Dict[str, Any], market: Market) -> Any: event_type = data["ev"] - if event_type in [EventType.EquityAgg.value, EventType.EquityAggMin.value]: - return EquityAgg.from_dict(data) - elif event_type in [ - EventType.CryptoAgg.value, - EventType.CryptoAggSec.value, - EventType.ForexAgg.value, - EventType.ForexAggSec.value, - ]: - return CurrencyAgg.from_dict(data) - elif event_type == EventType.EquityTrade.value: - return EquityTrade.from_dict(data) - elif event_type == EventType.CryptoTrade.value: - return CryptoTrade.from_dict(data) - elif event_type == EventType.EquityQuote.value: - return EquityQuote.from_dict(data) - elif event_type == EventType.ForexQuote.value: - return ForexQuote.from_dict(data) - elif event_type == EventType.CryptoQuote.value: - return CryptoQuote.from_dict(data) - elif event_type == EventType.Imbalances.value: - return Imbalance.from_dict(data) - elif event_type == EventType.LimitUpLimitDown.value: - return LimitUpLimitDown.from_dict(data) - elif event_type == EventType.CryptoL2.value: - return Level2Book.from_dict(data) - elif event_type == EventType.Value.value: - return IndexValue.from_dict(data) - elif event_type == EventType.LaunchpadValue.value: - return LaunchpadValue.from_dict(data) - elif event_type == EventType.BusinessFairMarketValue.value: - return FairMarketValue.from_dict(data) - return None + # Look up the model class based on market and event type + model_class = MARKET_EVENT_MAP.get(market, {}).get(event_type) + if model_class: + return model_class.from_dict(data) + else: + # Log a warning for unrecognized event types, unless it's a status message + if event_type != "status": + logger.warning("Unknown event type '%s' for market %s", event_type, market) + return None -def parse(msg: List[Dict[str, Any]], logger: logging.Logger) -> List[WebSocketMessage]: +def parse( + msg: List[Dict[str, Any]], logger: logging.Logger, market: Market +) -> List[WebSocketMessage]: res = [] for m in msg: - parsed = parse_single(m) + parsed = parse_single(m, market) if parsed is None: if m["ev"] != "status": logger.warning("could not parse message %s", m) diff --git a/polygon/websocket/models/common.py b/polygon/websocket/models/common.py index 38aea4c4..60892bad 100644 --- a/polygon/websocket/models/common.py +++ b/polygon/websocket/models/common.py @@ -11,7 +11,6 @@ class Feed(Enum): Launchpad = "launchpad.polygon.io" Business = "business.polygon.io" EdgxBusiness = "edgx-business.polygon.io" - IEXBusiness = "iex-business.polygon.io" DelayedBusiness = "delayed-business.polygon.io" DelayedEdgxBusiness = "delayed-edgx-business.polygon.io" DelayedNasdaqLastSaleBusiness = "delayed-nasdaq-last-sale-business.polygon.io" @@ -28,6 +27,7 @@ class Market(Enum): Forex = "forex" Crypto = "crypto" Indices = "indices" + Futures = "futures" class EventType(Enum): @@ -46,12 +46,10 @@ class EventType(Enum): LimitUpLimitDown = "LULD" CryptoL2 = "XL2" Value = "V" - """Launchpad* EventTypes are only available to Launchpad users. These values are the same across all asset classes ( - stocks, options, forex, crypto). - """ LaunchpadValue = "LV" LaunchpadAggMin = "AM" - """Business* EventTypes are only available to Business users. These values are the same across all asset classes ( - stocks, options, forex, crypto). - """ BusinessFairMarketValue = "FMV" + FuturesTrade = "T" + FuturesQuote = "Q" + FuturesAgg = "A" + FuturesAggMin = "AM" diff --git a/polygon/websocket/models/models.py b/polygon/websocket/models/models.py index d6fa0c29..9b95b302 100644 --- a/polygon/websocket/models/models.py +++ b/polygon/websocket/models/models.py @@ -359,6 +359,93 @@ def from_dict(d): ) +@modelclass +class FuturesTrade: + event_type: Optional[str] = None + symbol: Optional[str] = None + price: Optional[float] = None + size: Optional[int] = None + timestamp: Optional[int] = None + sequence_number: Optional[int] = None + + @staticmethod + def from_dict(d): + return FuturesTrade( + event_type=d.get("ev"), + symbol=d.get("sym"), + price=d.get("p"), + size=d.get("s"), + timestamp=d.get("t"), + sequence_number=d.get("q"), + ) + + +@modelclass +class FuturesQuote: + event_type: Optional[str] = None + symbol: Optional[str] = None + bid_price: Optional[float] = None + bid_size: Optional[int] = None + bid_timestamp: Optional[int] = None + ask_price: Optional[float] = None + ask_size: Optional[int] = None + ask_timestamp: Optional[int] = None + sip_timestamp: Optional[int] = None + + @staticmethod + def from_dict(d): + return FuturesQuote( + event_type=d.get("ev"), + symbol=d.get("sym"), + bid_price=d.get("bp"), + bid_size=d.get("bs"), + bid_timestamp=d.get("bt"), + ask_price=d.get("ap"), + ask_size=d.get("as"), + ask_timestamp=d.get("at"), + sip_timestamp=d.get("t"), + ) + + +@modelclass +class FuturesAgg: + event_type: Optional[str] = None + symbol: Optional[str] = None + volume: Optional[float] = None + accumulated_volume: Optional[float] = None + official_open_price: Optional[float] = None + vwap: Optional[float] = None + open: Optional[float] = None + close: Optional[float] = None + high: Optional[float] = None + low: Optional[float] = None + aggregate_vwap: Optional[float] = None + average_size: Optional[float] = None + start_timestamp: Optional[int] = None + end_timestamp: Optional[int] = None + otc: Optional[bool] = None # If present + + @staticmethod + def from_dict(d): + return FuturesAgg( + event_type=d.get("ev"), + symbol=d.get("sym"), + volume=d.get("v"), + accumulated_volume=d.get("av"), + official_open_price=d.get("op"), + vwap=d.get("vw"), + open=d.get("o"), + close=d.get("c"), + high=d.get("h"), + low=d.get("l"), + aggregate_vwap=d.get("a"), + average_size=d.get("z"), + start_timestamp=d.get("s"), + end_timestamp=d.get("e"), + otc=d.get("otc"), + ) + + WebSocketMessage = NewType( "WebSocketMessage", List[ @@ -376,6 +463,9 @@ def from_dict(d): IndexValue, LaunchpadValue, FairMarketValue, + FuturesTrade, + FuturesQuote, + FuturesAgg, ] ], ) From ad005b9d9de162fb38a4ae909c4b42880f746c84 Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Mon, 9 Jun 2025 01:58:36 -0700 Subject: [PATCH 05/12] Fix lint --- polygon/websocket/__init__.py | 2 +- polygon/websocket/models/__init__.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/polygon/websocket/__init__.py b/polygon/websocket/__init__.py index 9c4d4e4b..b7209148 100644 --- a/polygon/websocket/__init__.py +++ b/polygon/websocket/__init__.py @@ -58,7 +58,7 @@ def __init__( feed = feed.value if isinstance(market, Enum): market = market.value - self.url = f"ws{'s' if secure else ''}://{feed}/{market}" + self.url = f"ws{'s' if secure else ''}://{self.feed.value}/{self.market.value}" self.subscribed = False self.subs: Set[str] = set() self.max_reconnects = max_reconnects diff --git a/polygon/websocket/models/__init__.py b/polygon/websocket/models/__init__.py index 88d0829a..f6d1437f 100644 --- a/polygon/websocket/models/__init__.py +++ b/polygon/websocket/models/__init__.py @@ -50,10 +50,12 @@ } -def parse_single(data: Dict[str, Any], market: Market) -> Any: +def parse_single(data: Dict[str, Any], market: Market, logger: logging.Logger) -> Any: event_type = data["ev"] # Look up the model class based on market and event type - model_class = MARKET_EVENT_MAP.get(market, {}).get(event_type) + model_class: Optional[Type[FromDictProtocol]] = MARKET_EVENT_MAP.get( + market, {} + ).get(event_type) if model_class: return model_class.from_dict(data) else: From 413fef3fec213e7dbb9cd8cf4f4e606d27ce90e9 Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:41:11 -0700 Subject: [PATCH 06/12] Fix lint errors --- polygon/websocket/__init__.py | 11 +++++++---- polygon/websocket/models/__init__.py | 23 ++++++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/polygon/websocket/__init__.py b/polygon/websocket/__init__.py index b7209148..1304028f 100644 --- a/polygon/websocket/__init__.py +++ b/polygon/websocket/__init__.py @@ -49,16 +49,19 @@ def __init__( ) self.api_key = api_key self.feed = feed - self.market = market + if isinstance(market, str): + self.market = Market(market) # converts str input to enum + else: + self.market = market + + self.market_value = self.market.value self.raw = raw if verbose: logger.setLevel(logging.DEBUG) self.websocket_cfg = kwargs if isinstance(feed, Enum): feed = feed.value - if isinstance(market, Enum): - market = market.value - self.url = f"ws{'s' if secure else ''}://{self.feed.value}/{self.market.value}" + self.url = f"ws{'s' if secure else ''}://{feed}/{self.market_value}" self.subscribed = False self.subs: Set[str] = set() self.max_reconnects = max_reconnects diff --git a/polygon/websocket/models/__init__.py b/polygon/websocket/models/__init__.py index f6d1437f..38797dac 100644 --- a/polygon/websocket/models/__init__.py +++ b/polygon/websocket/models/__init__.py @@ -1,10 +1,18 @@ -from typing import Dict, Any, List +from typing import Dict, Any, List, Type, Protocol, cast from .common import * from .models import * import logging + +# Protocol to define classes with from_dict method +class FromDictProtocol(Protocol): + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "FromDictProtocol": + pass + + # Define the mapping of market and event type to model class -MARKET_EVENT_MAP = { +MARKET_EVENT_MAP: Dict[Market, Dict[str, Type[FromDictProtocol]]] = { Market.Stocks: { "A": EquityAgg, "AM": EquityAgg, @@ -50,14 +58,19 @@ } -def parse_single(data: Dict[str, Any], market: Market, logger: logging.Logger) -> Any: +def parse_single( + data: Dict[str, Any], logger: logging.Logger, market: Market +) -> Optional[WebSocketMessage]: event_type = data["ev"] # Look up the model class based on market and event type model_class: Optional[Type[FromDictProtocol]] = MARKET_EVENT_MAP.get( market, {} ).get(event_type) if model_class: - return model_class.from_dict(data) + parsed = model_class.from_dict(data) + return cast( + WebSocketMessage, parsed + ) # Ensure the return type is WebSocketMessage else: # Log a warning for unrecognized event types, unless it's a status message if event_type != "status": @@ -70,7 +83,7 @@ def parse( ) -> List[WebSocketMessage]: res = [] for m in msg: - parsed = parse_single(m, market) + parsed = parse_single(m, logger, market) if parsed is None: if m["ev"] != "status": logger.warning("could not parse message %s", m) From 0141c5678612b060e3fbd7298213785fb554d915 Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Mon, 9 Jun 2025 10:05:29 -0700 Subject: [PATCH 07/12] Make sure we support launchpad --- polygon/websocket/models/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/polygon/websocket/models/__init__.py b/polygon/websocket/models/__init__.py index 38797dac..20c02ce1 100644 --- a/polygon/websocket/models/__init__.py +++ b/polygon/websocket/models/__init__.py @@ -29,6 +29,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "FromDictProtocol": "T": EquityTrade, "Q": EquityQuote, "FMV": FairMarketValue, + "LV": LaunchpadValue, }, Market.Indices: { "A": EquityAgg, @@ -48,12 +49,16 @@ def from_dict(cls, data: Dict[str, Any]) -> "FromDictProtocol": "XQ": CryptoQuote, "XL2": Level2Book, "FMV": FairMarketValue, + "AM": EquityAgg, + "LV": LaunchpadValue, }, Market.Forex: { "CA": CurrencyAgg, "CAS": CurrencyAgg, "C": ForexQuote, "FMV": FairMarketValue, + "AM": EquityAgg, + "LV": LaunchpadValue, }, } From d88a30b7b6e6490535306ab85916de4e5659a4f6 Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Mon, 9 Jun 2025 12:34:33 -0700 Subject: [PATCH 08/12] Removed order param and added IEX back --- polygon/rest/futures.py | 12 ++---------- polygon/websocket/models/common.py | 1 + 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/polygon/rest/futures.py b/polygon/rest/futures.py index 8d765557..6defb823 100644 --- a/polygon/rest/futures.py +++ b/polygon/rest/futures.py @@ -33,7 +33,6 @@ def list_futures_aggregates( window_start_gt: Optional[str] = None, window_start_gte: Optional[str] = None, limit: Optional[int] = None, - order: Optional[Union[str, Order]] = None, sort: Optional[Union[str, Sort]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, @@ -66,7 +65,6 @@ def list_futures_contracts( active: Optional[str] = None, type: Optional[str] = None, limit: Optional[int] = None, - order: Optional[Union[str, Order]] = None, sort: Optional[Union[str, Sort]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, @@ -122,7 +120,6 @@ def list_futures_products( asset_sub_class: Optional[str] = None, type: Optional[str] = None, limit: Optional[int] = None, - order: Optional[Union[str, Order]] = None, sort: Optional[Union[str, Sort]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, @@ -181,7 +178,6 @@ def list_futures_quotes( session_end_date_gt: Optional[str] = None, session_end_date_gte: Optional[str] = None, limit: Optional[int] = None, - order: Optional[Union[str, Order]] = None, sort: Optional[Union[str, Sort]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, @@ -215,7 +211,6 @@ def list_futures_trades( session_end_date_gt: Optional[str] = None, session_end_date_gte: Optional[str] = None, limit: Optional[int] = None, - order: Optional[Union[str, Order]] = None, sort: Optional[Union[str, Sort]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, @@ -240,7 +235,6 @@ def list_futures_schedules( session_end_date: Optional[str] = None, market_identifier_code: Optional[str] = None, limit: Optional[int] = None, - order: Optional[Union[str, Order]] = None, sort: Optional[Union[str, Sort]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, @@ -270,18 +264,17 @@ def list_futures_schedules_by_product_code( session_end_date_gt: Optional[str] = None, session_end_date_gte: Optional[str] = None, limit: Optional[int] = None, - order: Optional[Union[str, Order]] = None, sort: Optional[Union[str, Sort]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, options: Optional[RequestOptionBuilder] = None, ) -> Union[Iterator[FuturesSchedule], HTTPResponse]: """ - Endpoint: GET /futures/vX/schedules/{product_code} + Endpoint: GET /futures/vX/products/{product_code}/schedules Returns schedule data for a single product across (potentially) many trading dates. """ - url = f"/futures/vX/schedules/{product_code}" + url = f"/futures/vX/products/{product_code}/schedules" return self._paginate( path=url, params=self._get_params( @@ -297,7 +290,6 @@ def list_futures_market_statuses( product_code_any_of: Optional[str] = None, product_code: Optional[str] = None, limit: Optional[int] = None, - order: Optional[Union[str, Order]] = None, sort: Optional[Union[str, Sort]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, diff --git a/polygon/websocket/models/common.py b/polygon/websocket/models/common.py index 60892bad..261f664b 100644 --- a/polygon/websocket/models/common.py +++ b/polygon/websocket/models/common.py @@ -11,6 +11,7 @@ class Feed(Enum): Launchpad = "launchpad.polygon.io" Business = "business.polygon.io" EdgxBusiness = "edgx-business.polygon.io" + IEXBusiness = "iex-business.polygon.io" DelayedBusiness = "delayed-business.polygon.io" DelayedEdgxBusiness = "delayed-edgx-business.polygon.io" DelayedNasdaqLastSaleBusiness = "delayed-nasdaq-last-sale-business.polygon.io" From 69f4229621be500d7f177034c0918ae69ec7f409 Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:31:57 -0700 Subject: [PATCH 09/12] Clean up imports --- polygon/rest/futures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polygon/rest/futures.py b/polygon/rest/futures.py index 6defb823..6de7669a 100644 --- a/polygon/rest/futures.py +++ b/polygon/rest/futures.py @@ -13,7 +13,7 @@ FuturesMarketStatus, FuturesSnapshot, ) -from .models.common import Sort, Order +from .models.common import Sort from .models.request import RequestOptionBuilder From 251922fbeae31160bad323d59cd36123e8940929 Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Sun, 6 Jul 2025 23:37:25 -0700 Subject: [PATCH 10/12] Sync futures client and models with spec --- polygon/rest/models/futures.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/polygon/rest/models/futures.py b/polygon/rest/models/futures.py index 8f6263e2..7d1f62cf 100644 --- a/polygon/rest/models/futures.py +++ b/polygon/rest/models/futures.py @@ -101,6 +101,7 @@ class FuturesProduct: market_identifier_code: Optional[str] = None asset_class: Optional[str] = None asset_sub_class: Optional[str] = None + clearing_channel: Optional[str] = None sector: Optional[str] = None sub_sector: Optional[str] = None type: Optional[str] = None @@ -122,6 +123,7 @@ def from_dict(d): as_of=d.get("as_of"), market_identifier_code=d.get("market_identifier_code"), asset_class=d.get("asset_class"), + clearing_channel=d.get("clearing_channel"), asset_sub_class=d.get("asset_sub_class"), sector=d.get("sector"), sub_sector=d.get("sub_sector"), From ee015a05ca6b7e7d95a8c54aba51ac8861fe2469 Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Mon, 7 Jul 2025 01:04:04 -0700 Subject: [PATCH 11/12] Added pagination flag and fixed base url parsing --- polygon/rest/__init__.py | 3 +++ polygon/rest/base.py | 15 ++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/polygon/rest/__init__.py b/polygon/rest/__init__.py index ed57ee72..46f2b98f 100644 --- a/polygon/rest/__init__.py +++ b/polygon/rest/__init__.py @@ -46,6 +46,7 @@ def __init__( num_pools: int = 10, retries: int = 3, base: str = BASE, + pagination: bool = True, verbose: bool = False, trace: bool = False, custom_json: Optional[Any] = None, @@ -57,6 +58,7 @@ def __init__( num_pools=num_pools, retries=retries, base=base, + pagination=pagination, verbose=verbose, trace=trace, custom_json=custom_json, @@ -68,6 +70,7 @@ def __init__( num_pools=num_pools, retries=retries, base=base, + pagination=pagination, verbose=verbose, trace=trace, custom_json=custom_json, diff --git a/polygon/rest/base.py b/polygon/rest/base.py index d9d4768a..76cf1430 100644 --- a/polygon/rest/base.py +++ b/polygon/rest/base.py @@ -10,7 +10,7 @@ from .models.request import RequestOptionBuilder from ..logging import get_logger import logging -from urllib.parse import urlencode +from urllib.parse import urlencode, urlparse from ..exceptions import AuthError, BadResponse logger = get_logger("RESTClient") @@ -30,6 +30,7 @@ def __init__( num_pools: int, retries: int, base: str, + pagination: bool, verbose: bool, trace: bool, custom_json: Optional[Any] = None, @@ -41,6 +42,7 @@ def __init__( self.API_KEY = api_key self.BASE = base + self.pagination = pagination self.headers = { "Authorization": "Bearer " + self.API_KEY, @@ -227,11 +229,14 @@ def _paginate_iter( return [] for t in decoded[result_key]: yield deserializer(t) - if "next_url" in decoded: - path = decoded["next_url"].replace(self.BASE, "") - params = {} - else: + if not self.pagination or "next_url" not in decoded: return + next_url = decoded["next_url"] + parsed = urlparse(next_url) + path = parsed.path + if parsed.query: + path += "?" + parsed.query + params = {} def _paginate( self, From eb8917a7c9169781665fb8c8d3621334d3dd20b6 Mon Sep 17 00:00:00 2001 From: justinpolygon <123573436+justinpolygon@users.noreply.github.com> Date: Mon, 7 Jul 2025 01:20:16 -0700 Subject: [PATCH 12/12] Added note about pagination flag --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index aaf7bb5f..7c0374e8 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,41 @@ for quote in quotes: print(quote) ``` +### Pagination Behavior + +By default, the client paginates results for endpoints like `list_trades` and `list_quotes` behind the scenes for you. Understanding how pagination interacts with the `limit` parameter is key. + +#### Default (Pagination Enabled) + +Pagination is enabled by default (`pagination=True`): + +* `limit` controls the page size, not the total number of results. +* The client automatically fetches all pages, yielding results until none remain. + +Here's an example: + +```python +client = RESTClient(api_key="") +trades = [t for t in client.list_trades(ticker="TSLA", limit=100)] +``` + +This fetches all TSLA trades, 100 per page. + +#### Disabling Pagination + +To return a fixed number of results and stop, disable pagination: + +```python +client = RESTClient(api_key="", pagination=False) +trades = [t for t in client.list_trades(ticker="TSLA", limit=100)] +``` + +This returns at most 100 total trades, no additional pages. + +### Performance Tip + +If you're fetching large datasets, always use the maximum supported limit for the API endpoint. This reduces the number of API calls and improves overall performance. + ### Additional Filter Parameters Many of the APIs in this client library support the use of additional filter parameters to refine your queries. Please refer to the specific API documentation for details on which filter parameters are supported for each endpoint. These filters can be applied using the following operators: