diff options
author | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-03-04 11:32:20 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-04 19:32:20 +0000 |
commit | 47541d4c957d7ab366e3b5f07615b4a7a9916c7e (patch) | |
tree | e00fff491dc9a71922e62ff13be7ea8958063636 | |
parent | eb07111477c8477b39f419c5d88cade9f0aaf8ab (diff) |
[BugFix] Fix FMP Market Snapshots (#6160)
* fix FMP market snapshots
* exception handling
* model type
* forgot one line
* fix test
---------
Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
8 files changed, 12579 insertions, 11896 deletions
diff --git a/openbb_platform/core/openbb_core/provider/standard_models/market_snapshots.py b/openbb_platform/core/openbb_core/provider/standard_models/market_snapshots.py index 4a3f622d04d..887ab74b0cf 100644 --- a/openbb_platform/core/openbb_core/provider/standard_models/market_snapshots.py +++ b/openbb_platform/core/openbb_core/provider/standard_models/market_snapshots.py @@ -4,7 +4,7 @@ from typing import Optional from pydantic import Field -from openbb_core.provider.abstract.data import Data +from openbb_core.provider.abstract.data import Data, ForceInt from openbb_core.provider.abstract.query_params import QueryParams from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS @@ -17,7 +17,6 @@ class MarketSnapshotsData(Data): """Market Snapshots Data.""" symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", "")) - open: Optional[float] = Field( description=DATA_DESCRIPTIONS.get("open", ""), default=None, @@ -34,14 +33,19 @@ class MarketSnapshotsData(Data): description=DATA_DESCRIPTIONS.get("close", ""), default=None, ) + volume: Optional[ForceInt] = Field( + description=DATA_DESCRIPTIONS.get("volume", ""), default=None + ) prev_close: Optional[float] = Field( description=DATA_DESCRIPTIONS.get("prev_close", ""), default=None, ) - change: Optional[float] = Field(description="The change in price.", default=None) - change_percent: Optional[float] = Field( - description="The change, as a percent.", default=None + change: Optional[float] = Field( + description="The change in price from the previous close.", + default=None, ) - volume: Optional[int] = Field( - description=DATA_DESCRIPTIONS.get("volume", ""), default=None + change_percent: Optional[float] = Field( + description="The change in price from the previous close, as a normalized percent.", + default=None, + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, ) diff --git a/openbb_platform/extensions/equity/integration/test_equity_api.py b/openbb_platform/extensions/equity/integration/test_equity_api.py index 66b8d5acb04..8bee0fe1569 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_api.py +++ b/openbb_platform/extensions/equity/integration/test_equity_api.py @@ -1674,7 +1674,7 @@ def test_equity_darkpool_otc(params, headers): @parametrize( "params", [ - ({"provider": "fmp", "market": "EURONEXT"}), + ({"provider": "fmp", "market": "euronext"}), ({"provider": "polygon"}), # premium endpoint ], ) diff --git a/openbb_platform/extensions/equity/integration/test_equity_python.py b/openbb_platform/extensions/equity/integration/test_equity_python.py index 46b11b30f3a..ff627eb82c2 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_python.py +++ b/openbb_platform/extensions/equity/integration/test_equity_python.py @@ -1571,7 +1571,7 @@ def test_equity_darkpool_otc(params, obb): @parametrize( "params", [ - ({"provider": "fmp", "market": "EURONEXT"}), + ({"provider": "fmp", "market": "euronext"}), ({"provider": "polygon"}), # premium endpoint ], ) diff --git a/openbb_platform/openbb/package/equity.py b/openbb_platform/openbb/package/equity.py index 01c8f5d8c65..18b065d1c69 100644 --- a/openbb_platform/openbb/package/equity.py +++ b/openbb_platform/openbb/package/equity.py @@ -85,7 +85,7 @@ class ROUTER_equity(Container): The provider to use for the query, by default None. If None, the provider specified in defaults is selected or 'fmp' if there is no default. - market : Literal['AMEX', 'AMS', 'ASE', 'ASX', 'ATH', 'BME', 'BRU', 'BUD', 'BUE', 'CAI', 'CNQ', 'CPH', 'DFM', 'DOH', 'DUS', 'ETF', 'EURONEXT', 'HEL', 'HKSE', 'ICE', 'IOB', 'IST', 'JKT', 'JNB', 'JPX', 'KLS', 'KOE', 'KSC', 'KUW', 'LSE', 'MEX', 'MIL', 'NASDAQ', 'NEO', 'NSE', 'NYSE', 'NZE', 'OSL', 'OTC', 'PNK', 'PRA', 'RIS', 'SAO', 'SAU', 'SES', 'SET', 'SGO', 'SHH', 'SHZ', 'SIX', 'STO', 'TAI', 'TLV', 'TSX', 'TWO', 'VIE', 'WSE', 'XETRA'] + market : Literal['amex', 'ams', 'ase', 'asx', 'ath', 'bme', 'bru', 'bud', 'bue', 'cai', 'cnq', 'cph', 'dfm', 'doh', 'etf', 'euronext', 'hel', 'hkse', 'ice', 'iob', 'ist', 'jkt', 'jnb', 'jpx', 'kls', 'koe', 'ksc', 'kuw', 'lse', 'mex', 'mutual_fund', 'nasdaq', 'neo', 'nse', 'nyse', 'nze', 'osl', 'otc', 'pnk', 'pra', 'ris', 'sao', 'sau', 'set', 'sgo', 'shh', 'shz', 'six', 'sto', 'tai', 'tlv', 'tsx', 'two', 'vie', 'wse', 'xetra'] The market to fetch data for. (provider: fmp) Returns @@ -114,18 +114,18 @@ class ROUTER_equity(Container): The low price. close : Optional[float] The close price. + volume : Optional[int] + The trading volume. prev_close : Optional[float] The previous close price. change : Optional[float] - The change in price. + The change in price from the previous close. change_percent : Optional[float] - The change, as a percent. - volume : Optional[int] - The trading volume. - price : Optional[float] + The change in price from the previous close, as a normalized percent. + last_price : Optional[float] The last price of the stock. (provider: fmp) - avg_volume : Optional[int] - Average volume of the stock. (provider: fmp) + last_price_timestamp : Optional[Union[date, datetime]] + The timestamp of the last price. (provider: fmp) ma50 : Optional[float] The 50-day moving average. (provider: fmp) ma200 : Optional[float] @@ -134,22 +134,22 @@ class ROUTER_equity(Container): The 52-week high. (provider: fmp) year_low : Optional[float] The 52-week low. (provider: fmp) - market_cap : Optional[float] + volume_avg : Optional[int] + Average daily trading volume. (provider: fmp) + market_cap : Optional[int] Market cap of the stock. (provider: fmp) - shares_outstanding : Optional[float] - Number of shares outstanding. (provider: fmp) eps : Optional[float] Earnings per share. (provider: fmp) pe : Optional[float] Price to earnings ratio. (provider: fmp) + shares_outstanding : Optional[int] + Number of shares outstanding. (provider: fmp) + name : Optional[str] + The company name associated with the symbol. (provider: fmp) exchange : Optional[str] The exchange of the stock. (provider: fmp) - timestamp : Optional[Union[float, int]] - The timestamp of the data. (provider: fmp) - earnings_announcement : Optional[str] - The earnings announcement of the stock. (provider: fmp) - name : Optional[str] - The name associated with the stock symbol. (provider: fmp) + earnings_date : Optional[Union[date, datetime]] + The upcoming earnings announcement date. (provider: fmp) vwap : Optional[float] The volume weighted average price of the stock on the current trading day. (provider: polygon) prev_open : Optional[float] @@ -454,7 +454,7 @@ class ROUTER_equity(Container): Filter by industry. (provider: fmp) country : Optional[str] Filter by country, as a two-letter country code. (provider: fmp) - exchange : Optional[Literal['amex', 'ase', 'asx', 'ath', 'bme', 'bru', 'bud', 'bue', 'cai', 'cnq', 'cph', 'dfm', 'doh', 'etf', 'euronext', 'hel', 'hkse', 'ice', 'iob', 'ist', 'jkt', 'jnb', 'jpx', 'kls', 'koe', 'ksc', 'kuw', 'lse', 'mex', 'nasdaq', 'neo', 'nse', 'nyse', 'nze', 'osl', 'otc', 'pnk', 'pra', 'ris', 'sao', 'sau', 'set', 'sgo', 'shh', 'shz', 'six', 'sto', 'tai', 'tlv', 'tsx', 'two', 'vie', 'wse', 'xetra']] + exchange : Optional[Literal['amex', 'ams', 'ase', 'asx', 'ath', 'bme', 'bru', 'bud', 'bue', 'cai', 'cnq', 'cph', 'dfm', 'doh', 'etf', 'euronext', 'hel', 'hkse', 'ice', 'iob', 'ist', 'jkt', 'jnb', 'jpx', 'kls', 'koe', 'ksc', 'kuw', 'lse', 'mex', 'mutual_fund', 'nasdaq', 'neo', 'nse', 'nyse', 'nze', 'osl', 'otc', 'pnk', 'pra', 'ris', 'sao', 'sau', 'set', 'sgo', 'shh', 'shz', 'six', 'sto', 'tai', 'tlv', 'tsx', 'two', 'vie', 'wse', 'xetra']] Filter by exchange. (provider: fmp) limit : Optional[int] Limit the number of results to return. (provider: fmp) diff --git a/openbb_platform/providers/fmp/openbb_fmp/models/market_snapshots.py b/openbb_platform/providers/fmp/openbb_fmp/models/market_snapshots.py index ac04fb7a719..89bdff1f42e 100644 --- a/openbb_platform/providers/fmp/openbb_fmp/models/market_snapshots.py +++ b/openbb_platform/providers/fmp/openbb_fmp/models/market_snapshots.py @@ -1,25 +1,31 @@ """FMP Market Snapshots Model.""" +from datetime import ( + date as dateType, + datetime, +) from typing import Any, Dict, List, Optional, Union +from dateutil import parser +from openbb_core.provider.abstract.data import ForceInt from openbb_core.provider.abstract.fetcher import Fetcher from openbb_core.provider.standard_models.market_snapshots import ( MarketSnapshotsData, MarketSnapshotsQueryParams, ) -from openbb_fmp.utils.definitions import MARKETS +from openbb_fmp.utils.definitions import EXCHANGES from openbb_fmp.utils.helpers import get_data -from pydantic import Field +from pydantic import Field, field_validator class FMPMarketSnapshotsQueryParams(MarketSnapshotsQueryParams): """FMP Market Snapshots Query. - Source: https://site.financialmodelingprep.com/developer/docs/#Most-of-the-EuroNext + Source: https://site.financialmodelingprep.com/developer/docs#exchange-prices-quote """ - market: MARKETS = Field( - description="The market to fetch data for.", default="NASDAQ" + market: EXCHANGES = Field( + description="The market to fetch data for.", default="nasdaq" ) @@ -31,14 +37,17 @@ class FMPMarketSnapshotsData(MarketSnapshotsData): "low": "dayLow", "prev_close": "previousClose", "change_percent": "changesPercentage", + "last_price": "price", + "last_price_timestamp": "timestamp", + "shares_outstanding": "sharesOutstanding", + "volume_avg": "avgVolume", } - price: Optional[float] = Field( + last_price: Optional[float] = Field( description="The last price of the stock.", default=None ) - - avg_volume: Optional[int] = Field( - description="Average volume of the stock.", alias="avgVolume", default=None + last_price_timestamp: Optional[Union[datetime, dateType]] = Field( + description="The timestamp of the last price.", default=None ) ma50: Optional[float] = Field( description="The 50-day moving average.", alias="priceAvg50", default=None @@ -52,30 +61,86 @@ class FMPMarketSnapshotsData(MarketSnapshotsData): year_low: Optional[float] = Field( description="The 52-week low.", alias="yearLow", default=None ) - market_cap: Optional[float] = Field( + volume_avg: Optional[ForceInt] = Field( + description="Average daily trading volume.", default=None + ) + market_cap: Optional[ForceInt] = Field( description="Market cap of the stock.", alias="marketCap", default=None ) - shares_outstanding: Optional[float] = Field( + eps: Optional[float] = Field(description="Earnings per share.", default=None) + pe: Optional[float] = Field(description="Price to earnings ratio.", default=None) + shares_outstanding: Optional[ForceInt] = Field( description="Number of shares outstanding.", - alias="sharesOutstanding", default=None, ) - eps: Optional[float] = Field(description="Earnings per share.", default=None) - pe: Optional[float] = Field(description="Price to earnings ratio.", default=None) + name: Optional[str] = Field( + description="The company name associated with the symbol.", default=None + ) exchange: Optional[str] = Field( description="The exchange of the stock.", default=None ) - timestamp: Optional[Union[int, float]] = Field( - description="The timestamp of the data.", default=None - ) - earnings_announcement: Optional[str] = Field( - description="The earnings announcement of the stock.", + earnings_date: Optional[Union[datetime, dateType]] = Field( + description="The upcoming earnings announcement date.", alias="earningsAnnouncement", default=None, ) - name: Optional[str] = Field( - description="The name associated with the stock symbol.", default=None + + @field_validator("last_price_timestamp", mode="before", check_fields=False) + @classmethod + def validate_timestamp(cls, v): + """Validate the timestamp.""" + if isinstance(v, (int, float)) and v != 0: + try: + v = datetime.fromtimestamp(v) + if v.hour == 0 and v.minute == 0 and v.second == 0: + v = v.date() + return v + except ValueError: + return None + return None + + @field_validator("earnings_date", mode="before", check_fields=False) + @classmethod + def date_validate(cls, v): # pylint: disable=E0213 + """Validate the ISO date string.""" + if v and ":" in str(v): + v = parser.isoparse(str(v)) + if v.hour == 0 and v.minute == 0 and v.second == 0: + return v.date() + return v + return parser.parse(str(v)).date() if v else None + + @field_validator("change_percent", mode="before", check_fields=False) + @classmethod + def normalize_percent(cls, v): + """Normalize the percent.""" + return float(v) / 100 if v else None + + @field_validator( + "shares_outstanding", + "volume", + "volume_avg", + "change", + "ma50", + "ma200", + "eps", + "pe", + "market_cap", + "year_high", + "year_low", + mode="before", + check_fields=False, ) + @classmethod + def validate_empty_numbers(cls, v): + """Validate empty fields.""" + return v if v != 0 else None + + @field_validator("name", mode="before", check_fields=False) + @classmethod + def validate_empty_strings(cls, v): + """Validate the name.""" + return v if v and v != " " and v != "''" else None class FMPMarketSnapshotsFetcher( diff --git a/openbb_platform/providers/fmp/openbb_fmp/utils/definitions.py b/openbb_platform/providers/fmp/openbb_fmp/utils/definitions.py index 503e001ad59..970b26db7d5 100644 --- a/openbb_platform/providers/fmp/openbb_fmp/utils/definitions.py +++ b/openbb_platform/providers/fmp/openbb_fmp/utils/definitions.py @@ -22,6 +22,7 @@ SECTORS = Literal[ EXCHANGES = Literal[ "amex", + "ams", "ase", "asx", "ath", @@ -50,6 +51,7 @@ EXCHANGES = Literal[ "kuw", "lse", "mex", + "mutual_fund", "nasdaq", "neo", "nse", @@ -110,6 +112,7 @@ MARKETS = Literal[ "LSE", "MEX", "MIL", + "MUTUAL_FUND", "NASDAQ", "NEO", "NSE", diff --git a/openbb_platform/providers/fmp/tests/record/http/test_fmp_fetchers/test_fmp_market_snapshots_fetcher.yaml b/openbb_platform/providers/fmp/tests/record/http/test_fmp_fetchers/test_fmp_market_snapshots_fetcher.yaml index ca5b24a214e..0e70fd67cb1 100644 --- a/openbb_platform/providers/fmp/tests/record/http/test_fmp_fetchers/test_fmp_market_snapshots_fetcher.yaml +++ b/openbb_platform/providers/fmp/tests/record/http/test_fmp_fetchers/test_fmp_market_snapshots_fetcher.yaml @@ -3,11862 +3,12473 @@ interactions: body: null headers: Accept: - - '*/*' + - application/json Accept-Encoding: - - gzip, deflate, br + - gzip, deflate Connection: - keep-alive method: GET - uri: https://financialmodelingprep.com/api/v3/quotes/LSE?apikey=MOCK_API_KEY + uri: https://financialmodelingprep.com/api/v3/quotes/lse?apikey=MOCK_API_KEY response: body: string: !!binary | - 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 |