diff --git a/massive/rest/base.py b/massive/rest/base.py index 8e2c4ac7..4962007c 100644 --- a/massive/rest/base.py +++ b/massive/rest/base.py @@ -208,16 +208,18 @@ def _get_params( elif isinstance(val, datetime): val = int(val.timestamp() * self.time_mult(datetime_res)) if val is not None: - for ext in ["lt", "lte", "gt", "gte", "any_of"]: + for ext in ["lt", "lte", "gt", "gte", "any_of", "all_of"]: if argname.endswith(f"_{ext}"): # lop off ext, then rebuild argname with ext, # using ., and not _ (removesuffix would work) # but that is python 3.9+ argname = argname[: -len(f"_{ext}")] + f".{ext}" - # any_of accepts a comma-separated str (per the spec) or a - # list of values; only join the latter, since joining a str - # would split it into single characters - if argname.endswith("any_of") and not isinstance(val, str): + # any_of/all_of accept a comma-separated str (per the spec) + # or a list of values; only join the latter, since joining + # a str would split it into single characters + if argname.endswith(("any_of", "all_of")) and not isinstance( + val, str + ): val = ",".join(val) params[argname] = val diff --git a/massive/rest/benzinga.py b/massive/rest/benzinga.py index fdd9d0b0..64811eae 100644 --- a/massive/rest/benzinga.py +++ b/massive/rest/benzinga.py @@ -139,6 +139,7 @@ def list_benzinga_consensus_ratings( date_lt: Optional[Union[str, date]] = None, date_lte: Optional[Union[str, date]] = None, limit: Optional[int] = None, + date_any_of: Optional[Union[str, List[str]]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, options: Optional[RequestOptionBuilder] = None, diff --git a/massive/rest/futures.py b/massive/rest/futures.py index 8973c7af..d8d77696 100644 --- a/massive/rest/futures.py +++ b/massive/rest/futures.py @@ -91,6 +91,8 @@ def list_futures_contracts( last_trade_date_lte: Optional[str] = None, limit: Optional[int] = None, sort: Optional[str] = None, + provider_id: Optional[str] = None, + provider_product_id: Optional[str] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, options: Optional[RequestOptionBuilder] = None, @@ -146,6 +148,7 @@ def list_futures_products( type_any_of: Optional[str] = None, limit: Optional[int] = None, sort: Optional[str] = None, + provider_id: Optional[str] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, options: Optional[RequestOptionBuilder] = None, diff --git a/massive/rest/models/aggs.py b/massive/rest/models/aggs.py index 49bba680..c689e519 100644 --- a/massive/rest/models/aggs.py +++ b/massive/rest/models/aggs.py @@ -107,6 +107,7 @@ class PreviousCloseAgg: timestamp: Optional[float] = None volume: Optional[float] = None vwap: Optional[float] = None + transactions: Optional[int] = None @staticmethod def from_dict(d): @@ -119,4 +120,5 @@ def from_dict(d): d.get("t", None), d.get("v", None), d.get("vw", None), + d.get("n", None), ) diff --git a/massive/rest/models/contracts.py b/massive/rest/models/contracts.py index f0ca648e..31a7aaff 100644 --- a/massive/rest/models/contracts.py +++ b/massive/rest/models/contracts.py @@ -51,3 +51,30 @@ def from_dict(d): ticker=d.get("ticker", None), underlying_ticker=d.get("underlying_ticker", None), ) + + +@modelclass +class OptionsOpenInterest: + "Daily open interest for a single options contract." + + ticker: Optional[str] = None + underlying_ticker: Optional[str] = None + root: Optional[str] = None + contract_type: Optional[str] = None + expiration_date: Optional[str] = None + strike_price: Optional[float] = None + date: Optional[str] = None + open_interest: Optional[int] = None + + @staticmethod + def from_dict(d): + return OptionsOpenInterest( + ticker=d.get("ticker"), + underlying_ticker=d.get("underlying_ticker"), + root=d.get("root"), + contract_type=d.get("contract_type"), + expiration_date=d.get("expiration_date"), + strike_price=d.get("strike_price"), + date=d.get("date"), + open_interest=d.get("open_interest"), + ) diff --git a/massive/rest/models/etf_global.py b/massive/rest/models/etf_global.py index 053babe6..f131a089 100644 --- a/massive/rest/models/etf_global.py +++ b/massive/rest/models/etf_global.py @@ -84,6 +84,7 @@ class EtfGlobalConstituent: asset_class: Optional[str] = None composite_ticker: Optional[str] = None constituent_name: Optional[str] = None + constituent_rank: Optional[int] = None constituent_ticker: Optional[str] = None country_of_exchange: Optional[str] = None currency_traded: Optional[str] = None @@ -105,6 +106,7 @@ def from_dict(d): asset_class=d.get("asset_class"), composite_ticker=d.get("composite_ticker"), constituent_name=d.get("constituent_name"), + constituent_rank=d.get("constituent_rank"), constituent_ticker=d.get("constituent_ticker"), country_of_exchange=d.get("country_of_exchange"), currency_traded=d.get("currency_traded"), diff --git a/massive/rest/models/financials.py b/massive/rest/models/financials.py index 3266da1d..f936acec 100644 --- a/massive/rest/models/financials.py +++ b/massive/rest/models/financials.py @@ -1306,3 +1306,91 @@ def from_dict(d): issuer_name=d.get("issuer_name"), ticker=d.get("ticker"), ) + + +@modelclass +class InsiderActivity: + "Daily insider trading activity for an issuer, aggregated across all of its insiders' open-market purchases and sales." + + issuer_cik: Optional[str] = None + issuer_name: Optional[str] = None + tickers: Optional[List[str]] = None + transaction_date: Optional[str] = None + last_filing_date: Optional[str] = None + purchase_count: Optional[int] = None + purchaser_count: Optional[int] = None + purchase_shares: Optional[float] = None + purchase_value: Optional[float] = None + sale_count: Optional[int] = None + seller_count: Optional[int] = None + sale_shares: Optional[float] = None + sale_value: Optional[float] = None + net_shares: Optional[float] = None + net_value: Optional[float] = None + planned_purchase_value: Optional[float] = None + planned_sale_value: Optional[float] = None + + @staticmethod + def from_dict(d): + return InsiderActivity( + issuer_cik=d.get("issuer_cik"), + issuer_name=d.get("issuer_name"), + tickers=d.get("tickers"), + transaction_date=d.get("transaction_date"), + last_filing_date=d.get("last_filing_date"), + purchase_count=d.get("purchase_count"), + purchaser_count=d.get("purchaser_count"), + purchase_shares=d.get("purchase_shares"), + purchase_value=d.get("purchase_value"), + sale_count=d.get("sale_count"), + seller_count=d.get("seller_count"), + sale_shares=d.get("sale_shares"), + sale_value=d.get("sale_value"), + net_shares=d.get("net_shares"), + net_value=d.get("net_value"), + planned_purchase_value=d.get("planned_purchase_value"), + planned_sale_value=d.get("planned_sale_value"), + ) + + +@modelclass +class InsiderHolding: + "An insider of an issuer (one reporting owner per issuer) with their current reported holdings." + + issuer_cik: Optional[str] = None + issuer_name: Optional[str] = None + tickers: Optional[List[str]] = None + owner_cik: Optional[str] = None + owner_name: Optional[str] = None + officer_title: Optional[str] = None + roles: Optional[List[str]] = None + shares_owned: Optional[float] = None + shares_owned_direct: Optional[float] = None + shares_owned_indirect: Optional[float] = None + ownership_pct: Optional[float] = None + position_count: Optional[int] = None + filing_count: Optional[int] = None + first_filing_date: Optional[str] = None + last_filing_date: Optional[str] = None + oldest_position_date: Optional[str] = None + + @staticmethod + def from_dict(d): + return InsiderHolding( + issuer_cik=d.get("issuer_cik"), + issuer_name=d.get("issuer_name"), + tickers=d.get("tickers"), + owner_cik=d.get("owner_cik"), + owner_name=d.get("owner_name"), + officer_title=d.get("officer_title"), + roles=d.get("roles"), + shares_owned=d.get("shares_owned"), + shares_owned_direct=d.get("shares_owned_direct"), + shares_owned_indirect=d.get("shares_owned_indirect"), + ownership_pct=d.get("ownership_pct"), + position_count=d.get("position_count"), + filing_count=d.get("filing_count"), + first_filing_date=d.get("first_filing_date"), + last_filing_date=d.get("last_filing_date"), + oldest_position_date=d.get("oldest_position_date"), + ) diff --git a/massive/rest/models/quotes.py b/massive/rest/models/quotes.py index 5f14d6b1..81940b9d 100644 --- a/massive/rest/models/quotes.py +++ b/massive/rest/models/quotes.py @@ -52,12 +52,13 @@ def from_dict(d): sequence_number=d.get("q", None), sip_timestamp=d.get("t", None), participant_timestamp=d.get("y", None), - ask_price=d.get("P", None), + # stocks send P/p; forex snapshot quotes send a/b + ask_price=d.get("P", d.get("a")), ask_size=d.get("S", None), ask_exchange=d.get("X", None), conditions=d.get("c", None), indicators=d.get("i", None), - bid_price=d.get("p", None), + bid_price=d.get("p", d.get("b")), bid_size=d.get("s", None), bid_exchange=d.get("x", None), tape=d.get("z", None), @@ -102,13 +103,15 @@ class RealTimeCurrencyConversion: initial_amount: Optional[float] = None last: Optional[ForexQuote] = None to: Optional[str] = None + symbol: Optional[str] = None @staticmethod def from_dict(d): return RealTimeCurrencyConversion( converted=d.get("converted", None), - from_=d.get("from_", None), + from_=d.get("from", d.get("from_")), initial_amount=d.get("initialAmount", None), last=None if "last" not in d else ForexQuote.from_dict(d["last"]), to=d.get("to", None), + symbol=d.get("symbol"), ) diff --git a/massive/rest/models/snapshot.py b/massive/rest/models/snapshot.py index 80b0bdb7..0e73894a 100644 --- a/massive/rest/models/snapshot.py +++ b/massive/rest/models/snapshot.py @@ -170,6 +170,8 @@ class LastQuoteOptionContractSnapshot: last_updated: Optional[int] = None midpoint: Optional[float] = None timeframe: Optional[str] = None + ask_exchange: Optional[int] = None + bid_exchange: Optional[int] = None @staticmethod def from_dict(d): diff --git a/massive/rest/models/tickers.py b/massive/rest/models/tickers.py index 2fda6c4f..775da553 100644 --- a/massive/rest/models/tickers.py +++ b/massive/rest/models/tickers.py @@ -112,6 +112,7 @@ class TickerDetails: name: Optional[str] = None phone_number: Optional[str] = None primary_exchange: Optional[str] = None + round_lot: Optional[int] = None share_class_figi: Optional[str] = None share_class_shares_outstanding: Optional[int] = None sic_code: Optional[str] = None @@ -147,6 +148,7 @@ def from_dict(d): name=d.get("name", None), phone_number=d.get("phone_number", None), primary_exchange=d.get("primary_exchange", None), + round_lot=d.get("round_lot", None), share_class_figi=d.get("share_class_figi", None), share_class_shares_outstanding=d.get( "share_class_shares_outstanding", None diff --git a/massive/rest/models/trades.py b/massive/rest/models/trades.py index 06cf8bd9..d4a41302 100644 --- a/massive/rest/models/trades.py +++ b/massive/rest/models/trades.py @@ -77,3 +77,34 @@ class CryptoTrade: @staticmethod def from_dict(d): return CryptoTrade(**d) + + +@modelclass +class OptionsUnusualTrade: + "An unusual trade for a US options contract." + + ticker: Optional[str] = None + underlying_ticker: Optional[str] = None + root: Optional[str] = None + sip_timestamp: Optional[int] = None + sequence_number: Optional[int] = None + price: Optional[float] = None + size: Optional[int] = None + exchange: Optional[int] = None + conditions: Optional[List[int]] = None + flags: Optional[List[str]] = None + + @staticmethod + def from_dict(d): + return OptionsUnusualTrade( + ticker=d.get("ticker"), + underlying_ticker=d.get("underlying_ticker"), + root=d.get("root"), + sip_timestamp=d.get("sip_timestamp"), + sequence_number=d.get("sequence_number"), + price=d.get("price"), + size=d.get("size"), + exchange=d.get("exchange"), + conditions=d.get("conditions"), + flags=d.get("flags"), + ) diff --git a/massive/rest/reference.py b/massive/rest/reference.py index b6886c55..babd7b7c 100644 --- a/massive/rest/reference.py +++ b/massive/rest/reference.py @@ -36,6 +36,9 @@ FilingForm3, FilingForm4, FilingIndex, + OptionsOpenInterest, + InsiderActivity, + InsiderHolding, ) from urllib3 import HTTPResponse from datetime import date @@ -552,6 +555,7 @@ def list_options_contracts( limit: Optional[int] = None, sort: Optional[Union[str, Sort]] = None, order: Optional[Union[str, Order]] = None, + ticker: Optional[str] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, options: Optional[RequestOptionBuilder] = None, @@ -603,6 +607,14 @@ def list_short_interest( limit: Optional[int] = None, sort: Optional[Union[str, Sort]] = None, order: Optional[Union[str, Order]] = None, + ticker_any_of: Optional[Union[str, List[str]]] = None, + ticker_gt: Optional[str] = None, + ticker_gte: Optional[str] = None, + ticker_lt: Optional[str] = None, + ticker_lte: Optional[str] = None, + days_to_cover_any_of: Optional[Union[str, List[str]]] = None, + settlement_date_any_of: Optional[Union[str, List[str]]] = None, + avg_daily_volume_any_of: Optional[Union[str, List[str]]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, options: Optional[RequestOptionBuilder] = None, @@ -666,6 +678,13 @@ def list_short_volume( limit: Optional[int] = None, sort: Optional[Union[str, Sort]] = None, order: Optional[Union[str, Order]] = None, + ticker_any_of: Optional[Union[str, List[str]]] = None, + ticker_gt: Optional[str] = None, + ticker_gte: Optional[str] = None, + ticker_lt: Optional[str] = None, + ticker_lte: Optional[str] = None, + date_any_of: Optional[Union[str, List[str]]] = None, + short_volume_ratio_any_of: Optional[Union[str, List[str]]] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, options: Optional[RequestOptionBuilder] = None, @@ -1206,3 +1225,123 @@ def list_stocks_filings_form_4( raw=raw, options=options, ) + + def list_options_open_interest( + self, + ticker: Optional[str] = None, + ticker_any_of: Optional[Union[str, List[str]]] = None, + ticker_gt: Optional[str] = None, + ticker_gte: Optional[str] = None, + ticker_lt: Optional[str] = None, + ticker_lte: Optional[str] = None, + root: Optional[str] = None, + root_any_of: Optional[Union[str, List[str]]] = None, + root_gt: Optional[str] = None, + root_gte: Optional[str] = None, + root_lt: Optional[str] = None, + root_lte: Optional[str] = None, + contract_type: Optional[str] = None, + contract_type_any_of: Optional[Union[str, List[str]]] = None, + expiration_date: Optional[Union[str, date]] = None, + expiration_date_gt: Optional[Union[str, date]] = None, + expiration_date_gte: Optional[Union[str, date]] = None, + expiration_date_lt: Optional[Union[str, date]] = None, + expiration_date_lte: Optional[Union[str, date]] = None, + date: Optional[Union[str, date]] = None, + date_gt: Optional[Union[str, date]] = None, + date_gte: Optional[Union[str, date]] = None, + date_lt: Optional[Union[str, date]] = None, + date_lte: Optional[Union[str, date]] = 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[OptionsOpenInterest], HTTPResponse]: + """ + Daily open interest for US options contracts, as reported by OPRA. + + Endpoint: GET /options/v1/open-interest + """ + url = "/options/v1/open-interest" + + return self._paginate( + path=url, + params=self._get_params(self.list_options_open_interest, locals()), + result_key="results", + deserializer=OptionsOpenInterest.from_dict, + raw=raw, + options=options, + ) + + def list_stocks_insider_activity( + self, + tickers: Optional[str] = None, + tickers_all_of: Optional[Union[str, List[str]]] = None, + tickers_any_of: Optional[Union[str, List[str]]] = None, + issuer_cik: Optional[str] = None, + issuer_cik_any_of: Optional[Union[str, List[str]]] = None, + transaction_date: Optional[Union[str, date]] = None, + transaction_date_gt: Optional[Union[str, date]] = None, + transaction_date_gte: Optional[Union[str, date]] = None, + transaction_date_lt: Optional[Union[str, date]] = None, + transaction_date_lte: Optional[Union[str, date]] = 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[InsiderActivity], HTTPResponse]: + """ + Daily insider trading activity per issuer: open-market purchases and sales by directors, officers and 10%+ + shareholders, aggregated from SEC Form 4 and Form 5 filings. + + Endpoint: GET /stocks/insiders/vX/activity + """ + url = "/stocks/insiders/vX/activity" + + return self._paginate( + path=url, + params=self._get_params(self.list_stocks_insider_activity, locals()), + result_key="results", + deserializer=InsiderActivity.from_dict, + raw=raw, + options=options, + ) + + def list_stocks_insider_holdings( + self, + tickers: Optional[str] = None, + tickers_all_of: Optional[Union[str, List[str]]] = None, + tickers_any_of: Optional[Union[str, List[str]]] = None, + issuer_cik: Optional[str] = None, + issuer_cik_any_of: Optional[Union[str, List[str]]] = None, + owner_cik: Optional[str] = None, + owner_cik_any_of: Optional[Union[str, List[str]]] = None, + last_filing_date: Optional[Union[str, date]] = None, + last_filing_date_gt: Optional[Union[str, date]] = None, + last_filing_date_gte: Optional[Union[str, date]] = None, + last_filing_date_lt: Optional[Union[str, date]] = None, + last_filing_date_lte: Optional[Union[str, date]] = 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[InsiderHolding], HTTPResponse]: + """ + Insiders of each issuer, one row per reporting owner, with their current reported holdings from SEC + Forms 3, 4 and 5. + + Endpoint: GET /stocks/insiders/vX/holdings + """ + url = "/stocks/insiders/vX/holdings" + + return self._paginate( + path=url, + params=self._get_params(self.list_stocks_insider_holdings, locals()), + result_key="results", + deserializer=InsiderHolding.from_dict, + raw=raw, + options=options, + ) diff --git a/massive/rest/snapshot.py b/massive/rest/snapshot.py index 1cf4e368..8e9e0c7b 100644 --- a/massive/rest/snapshot.py +++ b/massive/rest/snapshot.py @@ -35,6 +35,7 @@ def list_universal_snapshots( ticker_lte: Optional[str] = None, ticker_gt: Optional[str] = None, ticker_gte: Optional[str] = None, + ticker: Optional[str] = None, params: Optional[Dict[str, Any]] = None, raw: bool = False, options: Optional[RequestOptionBuilder] = None, diff --git a/massive/rest/trades.py b/massive/rest/trades.py index 3aad1d0e..34ab5d51 100644 --- a/massive/rest/trades.py +++ b/massive/rest/trades.py @@ -1,6 +1,6 @@ from .base import BaseClient -from typing import Optional, Any, Dict, Union, Iterator -from .models import Trade, LastTrade, CryptoTrade, Sort, Order +from typing import Optional, Any, Dict, List, Union, Iterator +from .models import Trade, LastTrade, CryptoTrade, OptionsUnusualTrade, Sort, Order from urllib3 import HTTPResponse from datetime import datetime, date @@ -102,3 +102,38 @@ def get_last_crypto_trade( raw=raw, options=options, ) + + def list_options_unusual_trades( + self, + ticker: Optional[str] = None, + ticker_any_of: Optional[Union[str, List[str]]] = None, + ticker_gt: Optional[str] = None, + ticker_gte: Optional[str] = None, + ticker_lt: Optional[str] = None, + ticker_lte: Optional[str] = None, + sip_timestamp: Optional[Union[str, int, datetime, date]] = None, + sip_timestamp_gt: Optional[Union[str, int, datetime, date]] = None, + sip_timestamp_gte: Optional[Union[str, int, datetime, date]] = None, + sip_timestamp_lt: Optional[Union[str, int, datetime, date]] = None, + sip_timestamp_lte: Optional[Union[str, int, datetime, date]] = 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[OptionsUnusualTrade], HTTPResponse]: + """ + Unusual trades for US options contracts, flagged from real-time OPRA trade data. + + Endpoint: GET /options/v1/unusual-trades + """ + url = "/options/v1/unusual-trades" + + return self._paginate( + path=url, + params=self._get_params(self.list_options_unusual_trades, locals()), + result_key="results", + deserializer=OptionsUnusualTrade.from_dict, + raw=raw, + options=options, + ) diff --git a/test_rest/test_aggs.py b/test_rest/test_aggs.py index 850a00da..b45ed7eb 100644 --- a/test_rest/test_aggs.py +++ b/test_rest/test_aggs.py @@ -108,6 +108,7 @@ def test_get_previous_close_agg(self): timestamp=1651003200000, volume=95595226.0, vwap=158.6074, + transactions=899965, ) ] self.assertEqual(aggs, expected) diff --git a/test_rest/test_quotes.py b/test_rest/test_quotes.py index 1086bdb5..c9028e1a 100644 --- a/test_rest/test_quotes.py +++ b/test_rest/test_quotes.py @@ -351,12 +351,13 @@ def test_get_real_time_currency_conversion(self): conversion = self.c.get_real_time_currency_conversion("AUD", "USD", 100, 2) expected = RealTimeCurrencyConversion( converted=69.31, - from_=None, + from_="AUD", initial_amount=100, last=ForexQuote( ask=1.4436264, bid=1.4427932, exchange=48, timestamp=1652195426000 ), to="USD", + symbol="USD/AUD", ) self.assertEqual(conversion, expected)