Skip to content

Commit

Permalink
Add NBBO Quotes, with historical, to obb.stocks.quote() (#5617)
Browse files Browse the repository at this point in the history
* coerce string type, add default=None

* add polygon stock quote

* limit cleanup

* add greater/less than to params

* limit param

* black

* tests

* fix tests

* test_etf

* rename model and function to NBBO

* add standard model for nbbo

* improve standardization

* not redifining builtin max

* fix input params

* integration test params

* removing unused import

Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>

* revamped code

* re-recorded nbbo test

* added symbol validator

* added alias in field

* Stock news -> Company news

---------

Co-authored-by: hjoaquim <h.joaquim@campus.fct.unl.pt>
Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>
Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>
  • Loading branch information
4 people committed Nov 8, 2023
1 parent f666a0b commit 82fc6ee
Show file tree
Hide file tree
Showing 9 changed files with 779 additions and 7 deletions.
40 changes: 40 additions & 0 deletions openbb_platform/extensions/stocks/integration/test_stocks_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,46 @@ def test_stocks_disc_filings(params, headers):
assert result.status_code == 200


@pytest.mark.parametrize(
"params",
[
(
{
"symbol": "CLOV",
"date": "2023-10-26",
"provider": "polygon",
"limit": 1000,
"timestamp_lte": None,
"timestamp_gte": None,
"timestamp_gt": None,
"timestamp_lt": None,
}
),
(
{
"symbol": "CLOV",
"provider": "polygon",
"timestamp_gt": "2023-10-26T15:20:00.000000000-04:00",
"timestamp_lt": "2023-10-26T15:30:00.000000000-04:00",
"limit": 5000,
"timestamp_gte": None,
"timestamp_lte": None,
"date": None,
}
),
],
)
@pytest.mark.integration
def test_stocks_nbbo(params, headers):
params = {p: v for p, v in params.items() if v}

query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/stocks/nbbo?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200


@pytest.mark.parametrize(
"params",
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,43 @@ def test_stocks_disc_filings(params, obb):
assert len(result.results) > 0


@pytest.mark.parametrize(
"params",
[
(
{
"symbol": "CLOV",
"date": "2023-10-26",
"provider": "polygon",
"limit": 1000,
"timestamp_lte": None,
"timestamp_gte": None,
"timestamp_gt": None,
"timestamp_lt": None,
}
),
(
{
"symbol": "CLOV",
"provider": "polygon",
"timestamp_gt": "2023-10-26T15:20:00.000000000-04:00",
"timestamp_lt": "2023-10-26T15:30:00.000000000-04:00",
"limit": 5000,
"timestamp_gte": None,
"timestamp_lte": None,
"date": None,
}
),
],
)
@pytest.mark.integration
def test_stocks_nbbo(params, obb):
result = obb.stocks.nbbo(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0


@pytest.mark.parametrize(
"params",
[
Expand Down
11 changes: 11 additions & 0 deletions openbb_platform/extensions/stocks/openbb_stocks/stocks_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ def info(
return OBBject(results=Query(**locals()).execute())


@router.command(model="StockNBBO")
def nbbo(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject[BaseModel]:
"""Stock Quote. Load stock data for a specific ticker."""
return OBBject(results=Query(**locals()).execute())


@router.command(model="StockFTD")
def ftd(
cc: CommandContext,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Stock NBBO data model."""

from pydantic import Field, field_validator

from openbb_provider.abstract.data import Data
from openbb_provider.abstract.query_params import QueryParams
from openbb_provider.utils.descriptions import QUERY_DESCRIPTIONS


class StockNBBOQueryParams(QueryParams):
"""Stock NBBO query model."""

symbol: str = Field(
description=QUERY_DESCRIPTIONS.get("symbol", ""),
)

@field_validator("symbol", mode="before", check_fields=False)
@classmethod
def upper_symbol(cls, v: str):
"""Convert symbol to uppercase."""
return v.upper()


class StockNBBOData(Data):
"""Stock NBBO data."""

ask_exchange: str = Field(
description="The exchange ID for the ask.",
)
ask: float = Field(
description="The last ask price.",
)
ask_size: int = Field(
description="""
The ask size. This represents the number of round lot orders at the given ask price.
The normal round lot size is 100 shares.
An ask size of 2 means there are 200 shares available to purchase at the given ask price.
""",
)
bid_size: int = Field(
description="The bid size in round lots.",
)
bid: float = Field(
description="The last bid price.",
)
bid_exchange: str = Field(
description="The exchange ID for the bid.",
)
2 changes: 2 additions & 0 deletions openbb_platform/providers/polygon/openbb_polygon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from openbb_polygon.models.market_snapshots import PolygonMarketSnapshotsFetcher
from openbb_polygon.models.stock_historical import PolygonStockHistoricalFetcher
from openbb_polygon.models.stock_nbbo import PolygonStockNBBOFetcher
from openbb_provider.abstract.provider import Provider

polygon_provider = Provider(
Expand All @@ -27,6 +28,7 @@
"CryptoHistorical": PolygonCryptoHistoricalFetcher,
"ForexHistorical": PolygonForexHistoricalFetcher,
"ForexPairs": PolygonForexPairsFetcher,
"StockNBBO": PolygonStockNBBOFetcher,
"IncomeStatement": PolygonIncomeStatementFetcher,
"MajorIndicesHistorical": PolygonMajorIndicesHistoricalFetcher,
"StockHistorical": PolygonStockHistoricalFetcher,
Expand Down
218 changes: 218 additions & 0 deletions openbb_platform/providers/polygon/openbb_polygon/models/stock_nbbo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
"""Polygon Stock NBBO Model."""

from datetime import (
date as dateType,
datetime,
)
from typing import Any, Dict, List, Optional, Union

from openbb_polygon.utils.helpers import get_data_one, map_tape
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.stock_nbbo import (
StockNBBOData,
StockNBBOQueryParams,
)
from openbb_provider.utils.descriptions import QUERY_DESCRIPTIONS
from openbb_provider.utils.helpers import get_querystring
from pandas import to_datetime
from pydantic import Field, field_validator


class PolygonStockNBBOQueryParams(StockNBBOQueryParams):
"""Polygon Stock NBBO query params.
Source: https://polygon.io/docs/stocks/get_v3_quotes__stockticker
"""

limit: int = Field(
default=50000,
description=(
QUERY_DESCRIPTIONS.get("limit", "")
+ " Up to ten million records will be returned. Pagination occurs in groups of 50,000."
+ " Remaining limit values will always return 50,000 more records unless it is the last page."
+ " High volume tickers will require multiple max requests for a single day's NBBO records."
+ " Expect stocks, like SPY, to approach 1GB in size, per day, as a raw CSV."
+ " Splitting large requests into chunks is recommended for full-day requests of high-volume symbols."
),
)
date: Optional[dateType] = Field(
default=None,
description=(
QUERY_DESCRIPTIONS.get("date", "")
+ " Use bracketed the timestamp parameters to specify exact time ranges."
),
alias="timestamp",
)
timestamp_lt: Optional[Union[datetime, str]] = Field(
default=None,
description="""
Query by datetime, less than. Either a date with the format YYYY-MM-DD or a TZ-aware timestamp string,
YYYY-MM-DDTH:M:S.000000000-04:00". Include all nanoseconds and the 'T' between the day and hour.
""",
)
timestamp_gt: Optional[Union[datetime, str]] = Field(
default=None,
description="""
Query by datetime, greater than. Either a date with the format YYYY-MM-DD or a TZ-aware timestamp string,
YYYY-MM-DDTH:M:S.000000000-04:00". Include all nanoseconds and the 'T' between the day and hour.
""",
)
timestamp_lte: Optional[Union[datetime, str]] = Field(
default=None,
description="""
Query by datetime, less than or equal to.
Either a date with the format YYYY-MM-DD or a TZ-aware timestamp string,
YYYY-MM-DDTH:M:S.000000000-04:00". Include all nanoseconds and the 'T' between the day and hour.
""",
)
timestamp_gte: Optional[Union[datetime, str]] = Field(
default=None,
description="""
Query by datetime, greater than or equal to.
Either a date with the format YYYY-MM-DD or a TZ-aware timestamp string,
YYYY-MM-DDTH:M:S.000000000-04:00". Include all nanoseconds and the 'T' between the day and hour.
""",
)

@field_validator("limit", mode="before", check_fields=False)
@classmethod
def capping_limit(cls, v):
"""Caps the number of records to 10 million."""
return 10000000 if v > 10000000 else v


class PolygonStockNBBOData(StockNBBOData):
"""Polygon Stock NBBO data."""

__alias_dict__ = {"ask": "ask_price", "bid": "bid_price"}

tape: Optional[str] = Field(
default=None, description="The exchange tape.", alias="tape_integer"
)
conditions: Optional[Union[str, List[int], List[str]]] = Field(
default=None, description="A list of condition codes.", alias="conditions"
)
indicators: Optional[List] = Field(
default=None, description="A list of indicator codes.", alias="indicators"
)
sequence_num: Optional[int] = Field(
default=None,
description="""
The sequence number represents the sequence in which message events happened.
These are increasing and unique per ticker symbol, but will not always be sequential
(e.g., 1, 2, 6, 9, 10, 11)
""",
alias="sequence_number",
)
participant_timestamp: Optional[datetime] = Field(
default=None,
description="""
The nanosecond accuracy Participant/Exchange Unix Timestamp.
This is the timestamp of when the quote was actually generated at the exchange.
""",
)
sip_timestamp: Optional[datetime] = Field(
default=None,
description="""
The nanosecond accuracy SIP Unix Timestamp.
This is the timestamp of when the SIP received this quote from the exchange which produced it.
""",
)
trf_timestamp: Optional[datetime] = Field(
default=None,
description="""
The nanosecond accuracy TRF (Trade Reporting Facility) Unix Timestamp.
This is the timestamp of when the trade reporting facility received this quote.
""",
)

@field_validator(
"sip_timestamp",
"participant_timestamp",
"trf_timestamp",
mode="before",
check_fields=False,
)
@classmethod
def date_validate(cls, v): # pylint: disable=E0213
"""Return formatted datetime."""
return (
to_datetime(v, unit="ns", origin="unix", utc=True).tz_convert("US/Eastern")
if v
else None
)


class PolygonStockNBBOFetcher(
Fetcher[PolygonStockNBBOQueryParams, List[PolygonStockNBBOData]]
):
"""Transform the query, extract and transform the data from the Polygon endpoints."""

@staticmethod
def transform_query(params: Dict[str, Any]) -> PolygonStockNBBOQueryParams:
"""Transform the query parameters."""
return PolygonStockNBBOQueryParams(**params)

@staticmethod
def extract_data(
query: PolygonStockNBBOQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Extract the data from the Polygon endpoint."""
api_key = credentials.get("polygon_api_key") if credentials else ""
data: List[Dict] = []
base_url = "https://api.polygon.io/v3"

query_str = get_querystring(
query.model_dump(by_alias=True), ["symbol", "limit"]
)
query_str = (
f"{query_str}&limit={query.limit}"
if query.limit <= 50000
else f"{query_str}&limit=50000"
)
query_str = query_str.replace("_", ".")

url = f"{base_url}/quotes/{query.symbol}?{query_str}&apiKey={api_key}"
response = get_data_one(url, **kwargs)
next_url = response.get("next_url", None)
data.extend(response["results"])
records = len(data)

while records < query.limit and next_url:
url = f"{next_url}&apiKey={api_key}"
response = get_data_one(url, **kwargs)
next_url = response.get("next_url", None)
data.extend(response["results"])
records += len(data)

exchanges_url = f"{base_url}/reference/exchanges?asset_class=stocks&locale=us&apiKey={api_key}"
exchanges = get_data_one(exchanges_url, **kwargs)
exchanges = exchanges["results"]
exchange_id_map = {e["id"]: e for e in exchanges}

data = [
{
**d,
"ask_exchange": exchange_id_map.get(d["ask_exchange"], {}).get(
"name", ""
),
"bid_exchange": exchange_id_map.get(d["bid_exchange"], {}).get(
"name", ""
),
"tape": map_tape(d.get("tape", "")),
}
for d in data
]

return data

@staticmethod
def transform_data(
query: PolygonStockNBBOQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[PolygonStockNBBOData]:
"""Transform the data."""
return [PolygonStockNBBOData.model_validate(d) for d in data]
Loading

0 comments on commit 82fc6ee

Please sign in to comment.