Skip to content

[WIP] Adds Futures support (alpha) #855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions polygon/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .rest import RESTClient
from .rest.base import version
from .websocket import WebSocketClient
from .rest.futures import FuturesClient
from .exceptions import *

__version__ = version
2 changes: 2 additions & 0 deletions polygon/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .snapshot import SnapshotClient
from .indicators import IndicatorsClient
from .summaries import SummariesClient
from .futures import FuturesClient
from .reference import (
MarketsClient,
TickersClient,
Expand Down Expand Up @@ -35,6 +36,7 @@ class RESTClient(
ContractsClient,
IndicatorsClient,
SummariesClient,
FuturesClient,
):
def __init__(
self,
Expand Down
409 changes: 409 additions & 0 deletions polygon/rest/futures.py

Large diffs are not rendered by default.

184 changes: 184 additions & 0 deletions polygon/rest/models/futures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
from typing import Optional, List
from ...modelclass import modelclass


@modelclass
class MarketExchanges:
"Contains exchange market status data."
nasdaq: Optional[str] = None
nyse: Optional[str] = None
otc: Optional[str] = None


@modelclass
class FuturesAggregate:
"""Represents an aggregate for a futures contract."""

close: Optional[float] = None
dollar_volume: Optional[float] = None
high: Optional[float] = None
low: Optional[float] = None
open: Optional[float] = None
ticker: Optional[str] = None
transaction_count: Optional[int] = None
underlying_asset: Optional[str] = None
volume: Optional[int] = None
window_end: Optional[int] = None
window_start: Optional[int] = None

@staticmethod
def from_dict(d):
return FuturesAggregate(
**{k: v for k, v in d.items() if k in FuturesAggregate.__annotations__}
)


@modelclass
class FuturesContract:
"""Represents a futures contract."""

active: Optional[bool] = None
as_of: Optional[str] = None
days_to_maturity: Optional[int] = None
exchange_code: Optional[str] = None
first_trade_date: Optional[str] = None
last_trade_date: Optional[str] = None
max_order_quantity: Optional[int] = None
min_order_quantity: Optional[int] = None
name: Optional[str] = None
product_code: Optional[str] = None
settlement_date: Optional[str] = None
settlement_tick_size: Optional[float] = None
spread_tick_size: Optional[float] = None
ticker: Optional[str] = None
trade_tick_size: Optional[float] = None
type: Optional[str] = None

@staticmethod
def from_dict(d):
return FuturesContract(
**{k: v for k, v in d.items() if k in FuturesContract.__annotations__}
)


@modelclass
class FuturesMarketStatus:
"""Represents the market status for a futures product."""

exchange_code: Optional[str] = None
market_status: Optional[str] = None
product_code: Optional[str] = None

@staticmethod
def from_dict(d):
return FuturesMarketStatus(
**{k: v for k, v in d.items() if k in FuturesMarketStatus.__annotations__}
)


@modelclass
class FuturesProduct:
"""Represents a futures product."""

as_of: Optional[str] = None
asset_class: Optional[str] = None
asset_sub_class: Optional[str] = None
exchange_code: Optional[str] = None
last_updated: Optional[str] = None
name: Optional[str] = None
otc_eligible: Optional[bool] = None
price_quotation: Optional[str] = None
product_code: Optional[str] = None
sector: Optional[str] = None
settlement_currency_code: Optional[str] = None
settlement_method: Optional[str] = None
settlement_type: Optional[str] = None
sub_sector: Optional[str] = None
trade_currency_code: Optional[str] = None
type: Optional[str] = None
unit_of_measure: Optional[str] = None
unit_of_measure_quantity: Optional[float] = None

@staticmethod
def from_dict(d):
return FuturesProduct(
**{k: v for k, v in d.items() if k in FuturesProduct.__annotations__}
)


@modelclass
class FuturesScheduleEvent:
"""Represents a single event in a futures schedule."""

event: Optional[str] = None
timestamp: Optional[str] = None

@staticmethod
def from_dict(d):
return FuturesScheduleEvent(
**{k: v for k, v in d.items() if k in FuturesScheduleEvent.__annotations__}
)


@modelclass
class FuturesSchedule:
"""Represents a futures trading schedule."""

market_identifier_code: Optional[str] = None
product_code: Optional[str] = None
product_name: Optional[str] = None
schedule: Optional[List[FuturesScheduleEvent]] = None
session_end_date: Optional[str] = None

@staticmethod
def from_dict(d):
schedule = (
[FuturesScheduleEvent.from_dict(e) for e in d.get("schedule", [])]
if d.get("schedule")
else None
)
return FuturesSchedule(
market_identifier_code=d.get("market_identifier_code"),
product_code=d.get("product_code"),
product_name=d.get("product_name"),
schedule=schedule,
session_end_date=d.get("session_end_date"),
)


@modelclass
class FuturesQuote:
"""Represents a quote for a futures contract."""

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
session_start_date: Optional[str] = None
ticker: Optional[str] = None
timestamp: Optional[int] = None

@staticmethod
def from_dict(d):
return FuturesQuote(
**{k: v for k, v in d.items() if k in FuturesQuote.__annotations__}
)


@modelclass
class FuturesTrade:
"""Represents a trade for a futures contract."""

price: Optional[float] = None
session_start_date: Optional[str] = None
size: Optional[float] = None
ticker: Optional[str] = None
timestamp: Optional[int] = None

@staticmethod
def from_dict(d):
return FuturesTrade(
**{k: v for k, v in d.items() if k in FuturesTrade.__annotations__}
)
2 changes: 1 addition & 1 deletion polygon/websocket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, self.market, logger)

if len(cmsg) > 0:
await processor(cmsg) # type: ignore
Expand Down
105 changes: 68 additions & 37 deletions polygon/websocket/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,82 @@
from typing import Dict, Any, List
from typing import Dict, Any, List, Optional
from .common import *
from .models import *
import logging

logger = logging.getLogger(__name__)

def parse_single(data: Dict[str, 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)

def parse_single(data: Dict[str, Any], market: Market) -> Optional[WebSocketMessage]:
event_type = data.get("ev")
if not event_type:
logger.warning("No event type ('ev') found in message data")
return None

if market == Market.Stocks:
if event_type == "T":
return EquityTrade.from_dict(data)
elif event_type == "Q":
return EquityQuote.from_dict(data)
elif event_type in ["A", "AM"]:
return EquityAgg.from_dict(data)
# Add more stock-specific events as needed (e.g., "LULD", "NOI")

elif market == Market.Options:
if event_type == "T":
return OptionTrade.from_dict(data)
elif event_type == "Q":
return OptionQuote.from_dict(data)
elif event_type in ["A", "AM"]:
return OptionAggregate.from_dict(data)

elif market == Market.Forex:
if event_type == "C":
return ForexQuote.from_dict(data)
elif event_type in ["CA", "CAS"]:
return ForexAggregate.from_dict(data)

elif market == Market.Crypto:
if event_type == "XT":
return CryptoTrade.from_dict(data)
elif event_type == "XQ":
return CryptoQuote.from_dict(data)
elif event_type in ["XA", "XAS"]:
return CryptoAggregate.from_dict(data)
elif event_type == "XL2":
return CryptoL2Book.from_dict(data)

elif market == Market.Indices:
if event_type in ["A", "AM"]:
return IndicesAggregate.from_dict(data)
elif event_type == "V":
return IndicesValue.from_dict(data)

elif market == Market.Futures:
if event_type == "T":
return FuturesTrade.from_dict(data)
elif event_type == "Q":
return FuturesQuote.from_dict(data)
elif event_type in ["A", "AM"]:
return FuturesAggregate.from_dict(data)

# Handle unknown markets
else:
logger.warning(f"Unknown market: {market}")
return None

# If event type is unrecognized within a known market
logger.warning(f"Unknown event type '{event_type}' for market '{market}'")
return None


def parse(msg: List[Dict[str, Any]], logger: logging.Logger) -> List[WebSocketMessage]:
def parse(
msg: List[Dict[str, Any]], market: Market, logger: logging.Logger
) -> List[WebSocketMessage]:
res = []
for m in msg:
parsed = parse_single(m)
parsed = parse_single(m, market)
if parsed is None:
if m["ev"] != "status":
if m.get("ev") != "status":
logger.warning("could not parse message %s", m)
else:
res.append(parsed)
Expand Down
11 changes: 10 additions & 1 deletion polygon/websocket/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

class Feed(Enum):
Delayed = "delayed.polygon.io"
RealTime = "socket.polygon.io"
RealTime = "socket.ny5.polygon.io" # "socket.polygon.io"
Nasdaq = "nasdaqfeed.polygon.io"
PolyFeed = "polyfeed.polygon.io"
PolyFeedPlus = "polyfeedplus.polygon.io"
Expand All @@ -28,6 +28,11 @@ class Market(Enum):
Forex = "forex"
Crypto = "crypto"
Indices = "indices"
Futures = "futures"
FuturesCME = "futures/cme"
FuturesCBOT = "futures/cbot"
FuturesNYMEX = "futures/nymex"
FuturesCOMEX = "futures/comex"


class EventType(Enum):
Expand All @@ -42,6 +47,10 @@ class EventType(Enum):
EquityQuote = "Q"
ForexQuote = "C"
CryptoQuote = "XQ"
FuturesTrade = "T"
FuturesQuote = "Q"
FuturesAggregateSecond = "A"
FuturesAggregateMinute = "AM"
Imbalances = "NOI"
LimitUpLimitDown = "LULD"
CryptoL2 = "XL2"
Expand Down
Loading
Loading