diff --git a/openbb_platform/core/openbb_core/provider/standard_models/equity_quote.py b/openbb_platform/core/openbb_core/provider/standard_models/equity_quote.py index d7c8354e2ebe..7c7168a3a5e6 100644 --- a/openbb_platform/core/openbb_core/provider/standard_models/equity_quote.py +++ b/openbb_platform/core/openbb_core/provider/standard_models/equity_quote.py @@ -18,7 +18,7 @@ class EquityQuoteQueryParams(QueryParams): symbol: str = Field( description=QUERY_DESCRIPTIONS.get("symbol", "") - + " In this case, the comma separated list of symbols." + + " This endpoint will accept multiple symbols separated by commas." ) @field_validator("symbol", mode="before", check_fields=False) @@ -33,14 +33,125 @@ def upper_symbol(cls, v: Union[str, List[str], Set[str]]): class EquityQuoteData(Data): """Equity Quote Data.""" - day_low: Optional[float] = Field( + symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", "")) + asset_type: Optional[str] = Field( + default=None, description="Type of asset - i.e, stock, ETF, etc." + ) + name: Optional[str] = Field( + default=None, description="Name of the company or asset." + ) + exchange: Optional[str] = Field( + default=None, + description="The name or symbol of the venue where the data is from.", + ) + bid: Optional[float] = Field( + default=None, description="Price of the top bid order." + ) + bid_size: Optional[int] = Field( + default=None, + description="This represents the number of round lot orders at the given price." + + " The normal round lot size is 100 shares." + + " A size of 2 means there are 200 shares available at the given price.", + ) + bid_exchange: Optional[str] = Field( + default=None, + description="The specific trading venue where the purchase order was placed.", + ) + ask: Optional[float] = Field( + default=None, description="Price of the top ask order." + ) + ask_size: Optional[int] = Field( + default=None, + description="This represents the number of round lot orders at the given price." + + " The normal round lot size is 100 shares." + + " A size of 2 means there are 200 shares available at the given price.", + ) + ask_exchange: Optional[str] = Field( + default=None, + description="The specific trading venue where the sale order was placed.", + ) + quote_conditions: Optional[Union[str, int, List[str], List[int]]] = Field( + default=None, + description="Conditions or condition codes applicable to the quote.", + ) + quote_indicators: Optional[Union[str, int, List[str], List[int]]] = Field( + default=None, + description="Indicators or indicator codes applicable to the participant" + + " quote related to the price bands for the issue, or the affect the quote has" + + " on the NBBO.", + ) + sales_conditions: Optional[Union[str, int, List[str], List[int]]] = Field( + default=None, + description="Conditions or condition codes applicable to the sale.", + ) + sequence_number: 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).", + ) + market_center: Optional[str] = Field( + default=None, + description="The ID of the UTP participant that originated the message.", + ) + participant_timestamp: Optional[datetime] = Field( default=None, - description="Lowest price of the stock in the current trading day.", + description="Timestamp for when the quote was generated by the exchange.", ) - day_high: Optional[float] = Field( + trf_timestamp: Optional[datetime] = Field( default=None, - description="Highest price of the stock in the current trading day.", + description="Timestamp for when the TRF (Trade Reporting Facility) received the message.", + ) + sip_timestamp: Optional[datetime] = Field( + default=None, + description="Timestamp for when the SIP (Security Information Processor)" + + " received the message from the exchange.", + ) + last_price: Optional[float] = Field( + default=None, description="Price of the last trade." + ) + last_tick: Optional[str] = Field( + default=None, description="Whether the last sale was an up or down tick." + ) + last_size: Optional[int] = Field( + default=None, description="Size of the last trade." + ) + last_timestamp: Optional[datetime] = Field( + default=None, description="Date and Time when the last price was recorded." + ) + open: Optional[float] = Field( + default=None, description=DATA_DESCRIPTIONS.get("open", "") + ) + high: Optional[float] = Field( + default=None, description=DATA_DESCRIPTIONS.get("high", "") + ) + low: Optional[float] = Field( + default=None, description=DATA_DESCRIPTIONS.get("low", "") + ) + close: Optional[float] = Field( + default=None, description=DATA_DESCRIPTIONS.get("close", "") + ) + volume: Optional[Union[int, float]] = Field( + default=None, description=DATA_DESCRIPTIONS.get("volume", "") + ) + exchange_volume: Optional[Union[int, float]] = Field( + default=None, + description="Volume of shares exchanged during the trading day on the specific exchange.", + ) + prev_close: Optional[float] = Field( + default=None, description=DATA_DESCRIPTIONS.get("prev_close", "") + ) + change: Optional[float] = Field( + default=None, description="Change in price from previous close." + ) + change_percent: Optional[float] = Field( + default=None, + description="Change in price as a normalized percentage.", + json_schema_extra={"x-frontendmultiply": 100}, + ) + year_high: Optional[float] = Field( + default=None, description="The one year high (52W High)." ) - date: Optional[datetime] = Field( - description=DATA_DESCRIPTIONS.get("date", ""), default=None + year_low: Optional[float] = Field( + default=None, description="The one year low (52W Low)." ) diff --git a/openbb_platform/providers/fmp/openbb_fmp/models/equity_quote.py b/openbb_platform/providers/fmp/openbb_fmp/models/equity_quote.py index e3eda38e7908..882eb26ea98a 100644 --- a/openbb_platform/providers/fmp/openbb_fmp/models/equity_quote.py +++ b/openbb_platform/providers/fmp/openbb_fmp/models/equity_quote.py @@ -1,7 +1,7 @@ """FMP Equity Quote Model.""" -from datetime import datetime -from typing import Any, Dict, List, Optional +from datetime import datetime, timezone +from typing import Any, Dict, List, Optional, Union from openbb_core.provider.abstract.data import ForceInt from openbb_core.provider.abstract.fetcher import Fetcher @@ -27,71 +27,58 @@ class FMPEquityQuoteData(EquityQuoteData): __alias_dict__ = { "price_avg50": "priceAvg50", "price_avg200": "priceAvg200", - "date": "timestamp", + "last_timestamp": "timestamp", + "high": "dayHigh", + "low": "dayLow", + "last_price": "price", + "change_percent": "changesPercentage", + "prev_close": "previousClose", } - - symbol: Optional[str] = Field(default=None, description="Symbol of the company.") - name: Optional[str] = Field(default=None, description="Name of the company.") - price: Optional[float] = Field( - default=None, description="Current trading price of the equity." - ) - changes_percentage: Optional[float] = Field( - default=None, description="Change percentage of the equity price." - ) - change: Optional[float] = Field( - default=None, description="Change in the equity price." - ) - year_high: Optional[float] = Field( - default=None, description="Highest price of the equity in the last 52 weeks." - ) - year_low: Optional[float] = Field( - default=None, description="Lowest price of the equity in the last 52 weeks." - ) - market_cap: Optional[float] = Field( - default=None, description="Market cap of the company." - ) price_avg50: Optional[float] = Field( - default=None, description="50 days average price of the equity." + default=None, description="50 day moving average price." ) price_avg200: Optional[float] = Field( - default=None, description="200 days average price of the equity." - ) - volume: Optional[ForceInt] = Field( - default=None, - description="Volume of the equity in the current trading day.", + default=None, description="200 day moving average price." ) avg_volume: Optional[ForceInt] = Field( default=None, - description="Average volume of the equity in the last 10 trading days.", - ) - exchange: Optional[str] = Field( - default=None, description="Exchange the equity is traded on." + description="Average volume over the last 10 trading days.", ) - open: Optional[float] = Field( - default=None, - description="Opening price of the equity in the current trading day.", - ) - previous_close: Optional[float] = Field( - default=None, description="Previous closing price of the equity." - ) - eps: Optional[float] = Field( - default=None, description="Earnings per share of the equity." - ) - pe: Optional[float] = Field( - default=None, description="Price earnings ratio of the equity." - ) - earnings_announcement: Optional[str] = Field( - default=None, description="Earnings announcement date of the equity." + market_cap: Optional[float] = Field( + default=None, description="Market cap of the company." ) shares_outstanding: Optional[ForceInt] = Field( - default=None, description="Number of shares outstanding of the equity." + default=None, description="Number of shares outstanding." + ) + eps: Optional[float] = Field(default=None, description="Earnings per share.") + pe: Optional[float] = Field(default=None, description="Price earnings ratio.") + earnings_announcement: Optional[Union[datetime, str]] = Field( + default=None, description="Upcoming earnings announcement date." ) - @field_validator("timestamp", mode="before", check_fields=False) + @field_validator("last_timestamp", mode="before", check_fields=False) @classmethod - def date_validate(cls, v): # pylint: disable=E0213 + def validate_last_timestamp(cls, v): # pylint: disable=E0213 """Return the date as a datetime object.""" - return datetime.strptime(v, "%Y-%m-%d") + v = int(v) if isinstance(v, str) else v + return datetime.utcfromtimestamp(int(v)).replace(tzinfo=timezone.utc) + + @field_validator("earnings_announcement", mode="before", check_fields=False) + @classmethod + def timestamp_validate(cls, v): # pylint: disable=E0213 + """Return the datetime string as a datetime object.""" + if v: + dt = datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%f%z") + dt = dt.replace(microsecond=0) + timestamp = dt.timestamp() + return datetime.fromtimestamp(timestamp, tz=timezone.utc) + return None + + @field_validator("change_percent", mode="after", check_fields=False) + @classmethod + def normalize_percent(cls, v): # pylint: disable=E0213 + """Return the percent value as a normalized value.""" + return float(v) / 100 if v else None class FMPEquityQuoteFetcher( diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/models/equity_quote.py b/openbb_platform/providers/intrinio/openbb_intrinio/models/equity_quote.py index 8e3c0676cdf0..6cdedf19460e 100644 --- a/openbb_platform/providers/intrinio/openbb_intrinio/models/equity_quote.py +++ b/openbb_platform/providers/intrinio/openbb_intrinio/models/equity_quote.py @@ -1,5 +1,6 @@ """Intrinio Equity Quote Model.""" - +# pylint: disable=unused-argument +import warnings from datetime import datetime from typing import Any, Dict, List, Optional @@ -12,9 +13,11 @@ ClientResponse, amake_requests, ) -from openbb_intrinio.utils.references import SOURCES +from openbb_intrinio.utils.references import SOURCES, VENUES, IntrinioSecurity from pydantic import Field, field_validator +_warn = warnings.warn + class IntrinioEquityQuoteQueryParams(EquityQuoteQueryParams): """Intrinio Equity Quote Query. @@ -32,68 +35,32 @@ class IntrinioEquityQuoteData(EquityQuoteData): """Intrinio Equity Quote Data.""" __alias_dict__ = { - "day_low": "low_price", - "day_high": "high_price", - "date": "last_time", + "exchange": "listing_venue", + "market_center": "market_center_code", + "bid": "bid_price", + "ask": "ask_price", + "open": "open_price", + "close": "close_price", + "low": "low_price", + "high": "high_price", + "last_timestamp": "last_time", + "volume": "market_volume", } - - last_price: float = Field(description="Price of the last trade.") - last_time: datetime = Field( - description="Date and Time when the last trade occurred.", alias="date" - ) - last_size: Optional[int] = Field(description="Size of the last trade.") - bid_price: float = Field(description="Price of the top bid order.") - bid_size: int = Field(description="Size of the top bid order.") - ask_price: float = Field(description="Price of the top ask order.") - ask_size: int = Field(description="Size of the top ask order.") - open_price: float = Field(description="Open price for the trading day.") - close_price: Optional[float] = Field( - default=None, description="Closing price for the trading day (IEX source only)." - ) - high_price: float = Field( - description="High Price for the trading day.", alias="day_high" - ) - low_price: float = Field( - description="Low Price for the trading day.", alias="day_low" - ) - exchange_volume: Optional[int] = Field( - default=None, - description="Number of shares exchanged during the trading day on the exchange.", + is_darkpool: Optional[bool] = Field( + default=None, description="Whether or not the current trade is from a darkpool." ) - market_volume: Optional[int] = Field( - default=None, - description="Number of shares exchanged during the trading day for the whole market.", + source: Optional[str] = Field( + default=None, description="Source of the Intrinio data." ) updated_on: datetime = Field( description="Date and Time when the data was last updated." ) - source: str = Field(description="Source of the data.") - listing_venue: Optional[str] = Field( - default=None, - description="Listing venue where the trade took place (SIP source only).", - ) - sales_conditions: Optional[str] = Field( - default=None, - description="Indicates any sales condition modifiers associated with the trade.", - ) - quote_conditions: Optional[str] = Field( - default=None, - description="Indicates any quote condition modifiers associated with the trade.", - ) - market_center_code: Optional[str] = Field( - default=None, description="Market center character code." - ) - is_darkpool: Optional[bool] = Field( - default=None, description="Whether or not the current trade is from a darkpool." - ) - messages: Optional[List[str]] = Field( - default=None, description="Messages associated with the endpoint." - ) - security: Optional[Dict[str, Any]] = Field( + security: Optional[IntrinioSecurity] = Field( default=None, description="Security details related to the quote." ) @field_validator("last_time", "updated_on", mode="before", check_fields=False) + @classmethod def date_validate(cls, v): # pylint: disable=E0213 """Return the date as a datetime object.""" return ( @@ -102,6 +69,25 @@ def date_validate(cls, v): # pylint: disable=E0213 else datetime.fromisoformat(v) ) + @field_validator("sales_conditions", mode="before", check_fields=False) + @classmethod + def validate_sales_conditions(cls, v): + """Validate sales conditions and remove empty strings.""" + if v == "\u0017 ": + return None + if v == "" or v is None: + return None + v = v.strip() + return v + + @field_validator("exchange", "market_center", mode="before", check_fields=False) + @classmethod + def validate_listing_venue(cls, v): + """Validate listing venue and remove empty strings.""" + if v: + return VENUES[v] if v in VENUES else v + return None + class IntrinioEquityQuoteFetcher( Fetcher[ @@ -133,20 +119,21 @@ async def callback(response: ClientResponse, _: Any) -> dict: return {} response_data = await response.json() - response_data["symbol"] = response.url.parts[-2] - - return response_data + response_data["symbol"] = response_data["security"].get("ticker", None) # type: ignore + if "messages" in response_data and response_data.get("messages"): # type: ignore + _message = list(response_data.pop("messages")) # type: ignore + _warn(str(",".join(_message))) + return response_data # type: ignore urls = [ f"{base_url}/securities/{s.strip()}/prices/realtime?source={query.source}&api_key={api_key}" for s in query.symbol.split(",") ] - return await amake_requests(urls, callback, **kwargs) @staticmethod def transform_data( - query: IntrinioEquityQuoteQueryParams, data: dict, **kwargs: Any + query: IntrinioEquityQuoteQueryParams, data: List[Dict], **kwargs: Any ) -> List[IntrinioEquityQuoteData]: """Return the transformed data.""" return [IntrinioEquityQuoteData.model_validate(d) for d in data] diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/utils/references.py b/openbb_platform/providers/intrinio/openbb_intrinio/utils/references.py index 876350ae34f1..fcd17a128dc2 100644 --- a/openbb_platform/providers/intrinio/openbb_intrinio/utils/references.py +++ b/openbb_platform/providers/intrinio/openbb_intrinio/utils/references.py @@ -17,6 +17,28 @@ "delayed_sip", ] +VENUES = { + "A": "NYSE MKT LLC", + "B": "NASDAQ OMX BX, Inc.", + "C": "National Stock Exchange Inc. (NSX)", + "D": "FINRA ADF", + "I": "International Securities Exchange, LLC", + "J": "Bats EDGA Exchange, INC", + "K": "Bats EDGX Exchange, Inc.", + "M": "Chicago Stock Exchange, Inc. (CHX)", + "N": "New York Stock Exchange LLC", + "P": "NYSE Arca, Inc.", + "S": "Consolidated Tape System", + "T": "NASDAQ (Tape A, B securities)", + "Q": "NASDAQ (Tape C securities)", + "V": "The Investors' Exchange, LLC (IEX)", + "W": "Chicago Broad Options Exchange, Inc. (CBOE)", + "X": "NASDAQ OMX PSX, Inc. LLC", + "Y": "Bats BYX Exchange, Inc.", + "Z": "Bats BZX Exchange, Inc.", + "u": "Other OTC Markets", +} + class IntrinioCompany(Data): """Intrinio Company Data.""" @@ -65,7 +87,7 @@ class IntrinioSecurity(Data): description="The country-composite OpenFIGI identifier.", default=None ) share_class_figi: Optional[str] = Field( - description="The global-composite OpenFIGI identifier.", + description="The global-composite OpenFIGI identifier.", default=None ) primary_listing: Optional[bool] = Field( description="""