diff --git a/openbb_providers/builtin_providers/__init__.py b/openbb_providers/builtin_providers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/openbb_providers/builtin_providers/benzinga/__init__.py b/openbb_providers/builtin_providers/benzinga/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/openbb_providers/builtin_providers/benzinga/global_news.py b/openbb_providers/builtin_providers/benzinga/global_news.py new file mode 100644 index 00000000000..0a19b97b705 --- /dev/null +++ b/openbb_providers/builtin_providers/benzinga/global_news.py @@ -0,0 +1,120 @@ +"""Benzinga Global News Fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.benzinga.helpers import BenzingaBaseNewsData, get_data + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import QueryParams +from openbb_provider.model.data.global_news import GlobalNewsData, GlobalNewsQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + + +class BenzingaGlobalNewsQueryParams(QueryParams): + """Benzinga Global News query. + + Source: https://docs.benzinga.io/benzinga/newsfeed-v2.html + + Parameter + --------- + page : int (default: 0) + The page of the data to retrieve. + pageSize : int (default: 15) + The number of results to return per page. + displayOutput : str (default: "headline") + The type of data to return. Options are "headline", "summary", "full", "all". + date : Optional[datetime] + The date of the news to retrieve. + dateFrom : Optional[datetime] + The start date of the news to retrieve. + dateTo : Optional[datetime] + The end date of the news to retrieve. + updatedSince : Optional[int] + The number of seconds since the news was updated. + publishedSince : Optional[int] + The number of seconds since the news was published. + sort : Optional[str] + The order in which to sort the news. Options are: + "published_at", "updated_at", "title", "author", "channel", "ticker", "topic", "content_type". + isin : Optional[str] + The ISIN of the news to retrieve. + cusip : Optional[str] + The CUSIP of the news to retrieve. + tickers : Optional[str] + The tickers of the news to retrieve. + channels : Optional[str] + The channels of the news to retrieve. + topics : Optional[str] + The topics of the news to retrieve. + authors : Optional[str] + The authors of the news to retrieve. + content_types : Optional[str] + The content types of the news to retrieve. + """ + + __name__ = "BenzingaGlobalNewsQueryParams" + page: int = Field(default=0) + pageSize: int = Field(default=15) + displayOutput: str = Field(default="headline") + date: Optional[datetime] = None + dateFrom: Optional[datetime] = None + dateTo: Optional[datetime] = None + updatedSince: Optional[int] = None + publishedSince: Optional[int] = None + sort: Optional[str] = None + isin: Optional[str] = None + cusip: Optional[str] = None + tickers: Optional[str] = None + channels: Optional[str] = None + topics: Optional[str] = None + authors: Optional[str] = None + content_types: Optional[str] = None + + +class BenzingaGlobalNewsData(BenzingaBaseNewsData): + __name__ = "BenzingaGlobalNewsData" + url: str + + +class BenzingaGlobalNewsFetcher( + Fetcher[ + GlobalNewsQueryParams, + GlobalNewsData, + BenzingaGlobalNewsQueryParams, + BenzingaGlobalNewsData, + ] +): + @staticmethod + def transform_query( + query: GlobalNewsQueryParams, extra_params: Optional[Dict] = None + ) -> BenzingaGlobalNewsQueryParams: + return BenzingaGlobalNewsQueryParams( + page=query.page, **extra_params if extra_params else {} + ) + + @staticmethod + def extract_data( + query: BenzingaGlobalNewsQueryParams, api_key: str + ) -> List[BenzingaGlobalNewsData]: + base_url = "https://api.benzinga.com/api/v2/news" + querystring = get_querystring(query.dict(), []) + request_url = f"{base_url}?{querystring}&token={api_key}" + data = get_data(request_url) + + if len(data) == 0: + raise RuntimeError("No news found") + + return [BenzingaGlobalNewsData(**d) for d in data] + + @staticmethod + def transform_data( + data: List[BenzingaGlobalNewsData], + ) -> List[GlobalNewsData]: + processors = {"image": lambda x: "" if x == [] else x[0].url} + return data_transformer(data, GlobalNewsData, processors) diff --git a/openbb_providers/builtin_providers/benzinga/helpers.py b/openbb_providers/builtin_providers/benzinga/helpers.py new file mode 100644 index 00000000000..8842aa0b27e --- /dev/null +++ b/openbb_providers/builtin_providers/benzinga/helpers.py @@ -0,0 +1,44 @@ +"""Benzinga Helpers.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import List + +# IMPORT THIRD-PARTY +import requests +from pydantic import BaseModel, Field, validator + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data + + +def get_data(url: str) -> dict: + r = requests.get(url, timeout=10, headers={"accept": "application/json"}) + if r.status_code != 200: + data = r.json() + if data == ['Access denied for user 0 "anonymous"']: + raise RuntimeError("API Key is invalid") + message = data.get("message") + error = data.get("error") + value = message or error + raise RuntimeError(f"Error in Benzinga request: {value}") + + return r.json() + + +class BenzingaImage(BaseModel): + size: str + url: str + + +class BenzingaBaseNewsData(Data): + __name__ = "BenzingaGlobalNewsData" + created: datetime = Field(alias="date") + title: str + image: List[BenzingaImage] + body: str = Field(alias="text") + url: str + + @validator("created", pre=True) + def time_validate(cls, v): # pylint: disable=E0213 + return datetime.strptime(v, "%a, %d %b %Y %H:%M:%S %z") diff --git a/openbb_providers/builtin_providers/benzinga/provider.py b/openbb_providers/builtin_providers/benzinga/provider.py new file mode 100644 index 00000000000..df5a55503e7 --- /dev/null +++ b/openbb_providers/builtin_providers/benzinga/provider.py @@ -0,0 +1,21 @@ +"""Benzinga provider module.""" + +# IMPORT STANDARD + +# IMPORT THIRD-PARTY + +from builtin_providers.benzinga.global_news import BenzingaGlobalNewsFetcher +from builtin_providers.benzinga.stock_news import BenzingaStockNewsFetcher + +# IMPORT INTERNAL +from openbb_provider.provider.abstract.provider import Provider + +# mypy: disable-error-code="list-item" + + +# ignoring because I dont know how to type the string properly +benzinga_provider = Provider( + name="benzinga", # type: ignore + description="Provider for Benzinga.", + fetcher_list=[BenzingaGlobalNewsFetcher, BenzingaStockNewsFetcher], +) diff --git a/openbb_providers/builtin_providers/benzinga/stock_news.py b/openbb_providers/builtin_providers/benzinga/stock_news.py new file mode 100644 index 00000000000..7a008637df8 --- /dev/null +++ b/openbb_providers/builtin_providers/benzinga/stock_news.py @@ -0,0 +1,119 @@ +"""Benzinga Stock News Fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.benzinga.helpers import BenzingaBaseNewsData, get_data + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import QueryParams +from openbb_provider.model.data.stock_news import StockNewsData, StockNewsQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + + +class BenzingaStockNewsQueryParams(QueryParams): + """Benzinga Stock News query. + + Source: https://docs.benzinga.io/benzinga/newsfeed-v2.html + + Parameter + --------- + page : int (default: 0) + The page of the data to retrieve. + symbols: str + The symbols of the companies. + pageSize : int (default: 15) + The number of results to return per page. + headline : str + The type of data to return. Options are "headline", "summary", "full", "all". + date : Optional[datetime] + The date of the news to retrieve. + dateFrom : Optional[datetime] + The start date of the news to retrieve. + dateTo : Optional[datetime] + The end date of the news to retrieve. + updatedSince : Optional[int] + The number of seconds since the news was updated. + publishedSince : Optional[int] + The number of seconds since the news was published. + sort : Optional[str] + The order in which to sort the news. Options are: + "published_at", "updated_at", "title", "author", "channel", "ticker", "topic", "content_type". + isin : Optional[str] + The ISIN of the news to retrieve. + cusip : Optional[str] + The CUSIP of the news to retrieve. + channels : Optional[str] + The channels of the news to retrieve. + topics : Optional[str] + The topics of the news to retrieve. + authors : Optional[str] + The authors of the news to retrieve. + content_types : Optional[str] + The content types of the news to retrieve. + """ + + __name__ = "BenzingaStockNewsQueryParams" + tickers: str = Field(alias="symbols") + page: int = Field(default=0) + pageSize: int = Field(default=15) + displayOutput: str = Field(default="headline") + date: Optional[datetime] = None + dateFrom: Optional[datetime] = None + dateTo: Optional[datetime] = None + updatedSince: Optional[int] = None + publishedSince: Optional[int] = None + sort: Optional[str] = None + isin: Optional[str] = None + cusip: Optional[str] = None + channels: Optional[str] = None + topics: Optional[str] = None + authors: Optional[str] = None + content_types: Optional[str] = None + + +class BenzingaStockNewsData(BenzingaBaseNewsData): + __name__ = "BenzingaStocklNewsData" + + +class BenzingaStockNewsFetcher( + Fetcher[ + StockNewsQueryParams, + StockNewsData, + BenzingaStockNewsQueryParams, + BenzingaStockNewsData, + ] +): + @staticmethod + def transform_query( + query: StockNewsQueryParams, extra_params: Optional[Dict] = None + ) -> BenzingaStockNewsQueryParams: + return BenzingaStockNewsQueryParams( + page=query.page, + symbols=query.symbols, # type: ignore + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: BenzingaStockNewsQueryParams, api_key: str + ) -> List[BenzingaStockNewsData]: + base_url = "https://api.benzinga.com/api/v2/news" + querystring = get_querystring(query.dict(), []) + request_url = f"{base_url}?{querystring}&token={api_key}" + data = get_data(request_url) + + if len(data) == 0: + raise RuntimeError("No news found") + + return [BenzingaStockNewsData(**d) for d in data] + + @staticmethod + def transform_data(data: List[BenzingaStockNewsData]) -> List[StockNewsData]: + processors = {"image": lambda x: "" if x == [] else x[0].url} + return data_transformer(data, StockNewsData, processors) diff --git a/openbb_providers/builtin_providers/fmp/__init__.py b/openbb_providers/builtin_providers/fmp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/openbb_providers/builtin_providers/fmp/analyst_estimates.py b/openbb_providers/builtin_providers/fmp/analyst_estimates.py new file mode 100644 index 00000000000..1b32cafe055 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/analyst_estimates.py @@ -0,0 +1,89 @@ +"""FMP Analyst Estimates fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many +from openbb_provider.model.abstract.data import Data + +# IMPORT INTERNAL +from openbb_provider.model.data.analyst_estimates import ( + AnalystEstimatesData, + AnalystEstimatesQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPAnalystEstimatesQueryParams(AnalystEstimatesQueryParams): + """FMP Analysts Estimates query. + + Source: https://site.financialmodelingprep.com/developer/docs/analyst-estimates-api/ + + Parameter + --------- + symbol : str + The symbol of the company. + limit : int + """ + + __name__ = "FMPAnalystEstimatesQueryParams" + + +class FMPAnalystEstimatesData(Data): + __name__ = "FMPAnalystEstimatesData" + + symbol: str + date: dateType + estimatedRevenueLow: int + estimatedRevenueHigh: int + estimatedRevenueAvg: int + estimatedEbitdaLow: int + estimatedEbitdaHigh: int + estimatedEbitdaAvg: int + estimatedEbitLow: int + estimatedEbitHigh: int + estimatedEbitAvg: int + estimatedNetIncomeLow: int + estimatedNetIncomeHigh: int + estimatedNetIncomeAvg: int + estimatedSgaExpenseLow: int + estimatedSgaExpenseHigh: int + estimatedSgaExpenseAvg: int + estimatedEpsAvg: float + estimatedEpsHigh: float + estimatedEpsLow: float + numberAnalystEstimatedRevenue: int + numberAnalystsEstimatedEps: int + + +class FMPAnalystEstimatesFetcher( + Fetcher[ + AnalystEstimatesQueryParams, + AnalystEstimatesData, + FMPAnalystEstimatesQueryParams, + FMPAnalystEstimatesData, + ] +): + @staticmethod + def transform_query( + query: AnalystEstimatesQueryParams, extra_params: Optional[Dict] = None + ) -> FMPAnalystEstimatesQueryParams: + return FMPAnalystEstimatesQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPAnalystEstimatesQueryParams, api_key: str + ) -> List[FMPAnalystEstimatesData]: + url = create_url( + 3, f"analyst-estimates/{query.symbol}", api_key, query, ["symbol"] + ) + return get_data_many(url, FMPAnalystEstimatesData) + + @staticmethod + def transform_data( + data: List[FMPAnalystEstimatesData], + ) -> List[AnalystEstimatesData]: + return data_transformer(data, AnalystEstimatesData) diff --git a/openbb_providers/builtin_providers/fmp/balance_sheet.py b/openbb_providers/builtin_providers/fmp/balance_sheet.py new file mode 100644 index 00000000000..7ecc4915a2c --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/balance_sheet.py @@ -0,0 +1,169 @@ +"""FMP Balance Sheet Fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from datetime import datetime +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeInt, root_validator + +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.balance_sheet import ( + BalanceSheetData, + BalanceSheetQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher + + +class FMPBalanceSheetQueryParams(QueryParams): + """FMP Balance Sheet QueryParams. + + Source: https://financialmodelingprep.com/developer/docs/#Balance-Sheet + + Parameter + --------- + symbol : Optional[str] + The symbol of the company if cik is not provided. + cik : Optional[str] + The CIK of the company if symbol is not provided. + period : Literal["annual", "quarter"] + The period of the balance sheet. Default is "annual". + limit : Optional[NonNegativeInt] + The limit of the balance sheet. + """ + + __name__ = "FMPBalanceSheetQueryParams" + symbol: Optional[str] + cik: Optional[str] + period: Literal["annual", "quarter"] = Field(default="annual") + limit: Optional[NonNegativeInt] + + @root_validator() + def check_symbol_or_cik(cls, values): # pylint: disable=no-self-argument + if values.get("symbol") is None and values.get("cik") is None: + raise ValueError("symbol or cik must be provided") + return values + + +class FMPBalanceSheetData(Data): + __name__ = "FMPBalanceSheetData" + date: dateType + symbol: str + cik: Optional[int] + reportedCurrency: Optional[str] + fillingDate: Optional[dateType] + acceptedDate: Optional[datetime] + calendarYear: Optional[int] + period: Optional[str] + cashAndCashEquivalents: Optional[int] + shortTermInvestments: Optional[int] + cashAndShortTermInvestments: Optional[int] + netReceivables: Optional[int] + inventory: Optional[int] + otherCurrentAssets: Optional[int] + totalCurrentAssets: Optional[int] + propertyPlantEquipmentNet: Optional[int] + goodwill: Optional[int] + intangibleAssets: Optional[int] + goodwillAndIntangibleAssets: Optional[int] + longTermInvestments: Optional[int] + taxAssets: Optional[int] + otherNonCurrentAssets: Optional[int] + totalNonCurrentAssets: Optional[int] + otherAssets: Optional[int] + totalAssets: Optional[int] + accountPayables: Optional[int] + shortTermDebt: Optional[int] + taxPayables: Optional[int] + deferredRevenue: Optional[int] + otherCurrentLiabilities: Optional[int] + totalCurrentLiabilities: Optional[int] + longTermDebt: Optional[int] + deferredRevenueNonCurrent: Optional[int] + deferredTaxLiabilitiesNonCurrent: Optional[int] + otherNonCurrentLiabilities: Optional[int] + totalNonCurrentLiabilities: Optional[int] + otherLiabilities: Optional[int] + capitalLeaseObligations: Optional[int] + totalLiabilities: Optional[int] + preferredStock: Optional[int] + commonStock: Optional[int] + retainedEarnings: Optional[int] + accumulatedOtherComprehensiveIncomeLoss: Optional[int] + othertotalStockholdersEquity: Optional[int] + totalStockholdersEquity: Optional[int] + totalLiabilitiesAndStockholdersEquity: Optional[int] + minorityInterest: Optional[int] + totalEquity: Optional[int] + totalLiabilitiesAndTotalEquity: Optional[int] + totalInvestments: Optional[int] + netDebt: Optional[int] + finalLink: Optional[str] + + +class FMPBalanceSheetFetcher( + Fetcher[ + BalanceSheetQueryParams, + BalanceSheetData, + FMPBalanceSheetQueryParams, + FMPBalanceSheetData, + ] +): + @staticmethod + def transform_query( + query: BalanceSheetQueryParams, extra_params: Optional[Dict] = None + ) -> FMPBalanceSheetQueryParams: + period = "annual" if query.period == "annually" else "quarter" + return FMPBalanceSheetQueryParams( + symbol=query.symbol, period=period, **extra_params if extra_params else {} # type: ignore + ) + + @staticmethod + def extract_data( + query: FMPBalanceSheetQueryParams, api_key: str + ) -> List[FMPBalanceSheetData]: + url = create_url( + 3, f"balance-sheet-statement/{query.symbol}", api_key, query, ["symbol"] + ) + return get_data_many(url, FMPBalanceSheetData) + + @staticmethod + def transform_data( + data: List[FMPBalanceSheetData], + ) -> List[BalanceSheetData]: + final_items = [] + for item in data: + tot_se = item.totalStockholdersEquity + minority = item.minorityInterest + parent: Optional[int] = None + if tot_se: + if tot_se is None: + pass + elif minority is None: + parent = tot_se + else: + parent = tot_se - minority + + final = BalanceSheetData( + date=item.date, + # symbol=item.symbol, + # currency=item.reportedCurrency, + cik=item.cik, + # period=item.period, + assets=item.totalAssets, + current_assets=item.totalCurrentAssets, + current_liabilities=item.totalCurrentLiabilities, + equity=item.totalStockholdersEquity, + equity_attributable_to_noncontrolling_interest=item.minorityInterest, + equity_attributable_to_parent=parent, + liabilities=item.totalLiabilities, + liabilities_and_equity=item.totalLiabilitiesAndTotalEquity, + noncurrent_assets=item.totalNonCurrentAssets, + noncurrent_liabilities=item.totalNonCurrentLiabilities, + ) + final_items.append(final) + return final_items diff --git a/openbb_providers/builtin_providers/fmp/cash_flow.py b/openbb_providers/builtin_providers/fmp/cash_flow.py new file mode 100644 index 00000000000..db6607c45b7 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/cash_flow.py @@ -0,0 +1,133 @@ +"""FMP Cash Flow Statement Fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from datetime import datetime +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeInt, root_validator + +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.cash_flows import ( + CashFlowStatementData, + CashFlowStatementQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPCashFlowStatementQueryParams(QueryParams): + """FMP Cash Flow Statement QueryParams. + + Source: https://financialmodelingprep.com/developer/docs/#Cash-Flow-Statement + + Parameter + --------- + symbol : Optional[str] + The symbol of the company if cik is not provided. + cik : Optional[str] + The CIK of the company if symbol is not provided. + period : Literal["annual", "quarter"] + The period of the cash flow statement. Default is "annual". + limit : Optional[NonNegativeInt] + The limit of the cash flow statement. + """ + + __name__ = "FMPCashFlowStatementQueryParams" + symbol: Optional[str] + cik: Optional[str] + period: Literal["annual", "quarter"] = Field(default="annual") + limit: Optional[NonNegativeInt] + + @root_validator() + def check_symbol_or_cik(cls, values): # pylint: disable=E0213 + if values.get("symbol") is None and values.get("cik") is None: + raise ValueError("symbol or cik must be provided") + return values + + +class FMPCashFlowStatementData(Data): + __name__ = "FMPCashFlowStatementData" + date: dateType + symbol: str + cik: Optional[int] + reportedCurrency: str = Field(alias="currency") + fillingDate: Optional[dateType] + acceptedDate: Optional[datetime] + calendarYear: Optional[int] + period: Optional[str] + netIncome: Optional[int] + depreciationAndAmortization: Optional[int] + deferredIncomeTax: Optional[int] + stockBasedCompensation: Optional[int] + changeInWorkingCapital: Optional[int] + accountsReceivables: Optional[int] + inventory: Optional[int] + accountsPayables: Optional[int] + otherWorkingCapital: Optional[int] + otherNonCashItems: Optional[int] + netCashProvidedByOperatingActivities: Optional[int] = Field( + alias="net_cash_flow_from_operating_activities" + ) + investmentsInPropertyPlantAndEquipment: Optional[int] + acquisitionsNet: Optional[int] + purchasesOfInvestments: Optional[int] + salesMaturitiesOfInvestments: Optional[int] + otherInvestingActivites: Optional[int] + netCashUsedForInvestingActivites: Optional[int] = Field( + alias="net_cash_flow_from_investing_activities" + ) + debtRepayment: Optional[int] + commonStockIssued: Optional[int] + commonStockRepurchased: Optional[int] + dividendsPaid: Optional[int] + otherFinancingActivites: Optional[int] + netCashUsedProvidedByFinancingActivities: Optional[int] = Field( + alias="net_cash_flow_from_financing_activities" + ) + effectOfForexChangesOnCash: Optional[int] = Field(alias="exchange_gain_losses") + netChangeInCash: Optional[int] = Field(alias="net_cash_flow") + cashAtEndOfPeriod: Optional[int] + cashAtBeginningOfPeriod: Optional[int] + operatingCashFlow: Optional[int] + capitalExpenditure: Optional[int] + freeCashFlow: Optional[int] + link: Optional[str] + finalLink: Optional[str] + + +class FMPCashFlowStatementFetcher( + Fetcher[ + CashFlowStatementQueryParams, + CashFlowStatementData, + FMPCashFlowStatementQueryParams, + FMPCashFlowStatementData, + ] +): + @staticmethod + def transform_query( + query: CashFlowStatementQueryParams, extra_params: Optional[Dict] = None + ) -> FMPCashFlowStatementQueryParams: + period = "annual" if query.period == "annually" else "quarter" + return FMPCashFlowStatementQueryParams( + symbol=query.symbol, period=period, **extra_params if extra_params else {} # type: ignore + ) + + @staticmethod + def extract_data( + query: FMPCashFlowStatementQueryParams, api_key: str + ) -> List[FMPCashFlowStatementData]: + url = create_url( + 3, f"cash-flow-statement/{query.symbol}", api_key, query, ["symbol"] + ) + return get_data_many(url, FMPCashFlowStatementData) + + @staticmethod + def transform_data( + data: List[FMPCashFlowStatementData], + ) -> List[CashFlowStatementData]: + return data_transformer(data, CashFlowStatementData) diff --git a/openbb_providers/builtin_providers/fmp/company_profile.py b/openbb_providers/builtin_providers/fmp/company_profile.py new file mode 100644 index 00000000000..e4a787fbb0b --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/company_profile.py @@ -0,0 +1,108 @@ +"""FMP Company Overview Fetcher.""" + +# IMPORT STANDARD +from typing import Dict, Optional + +from builtin_providers.fmp.helpers import get_data +from openbb_provider.model.abstract.data import Data + +# IMPORT INTERNAL +from openbb_provider.model.data.company_overview import ( + CompanyOverviewData, + CompanyOverviewQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + +# IMPORT THIRD-PARTY + + +# This part is only provided by FMP and not by the other providers for now. + + +class FMPCompanyOverviewQueryParams(CompanyOverviewQueryParams): + """FMP Company Overview query. + + Source: https://site.financialmodelingprep.com/developer/docs/companies-key-stats-free-api/ + + Parameter + --------- + symbol : str + The symbol of the company. + """ + + __name__ = "FMPCompanyOverviewQueryParams" + + +class FMPCompanyOverviewData(Data): + __name__ = "FMPCompanyOverviewData" + symbol: str + price: float + beta: float + volAvg: int + mktCap: int + lastDiv: float + range: str + changes: float + companyName: str + currency: str + cik: Optional[str] + isin: Optional[str] + cusip: Optional[str] + exchange: str + exchangeShortName: str + industry: str + website: str + description: str + ceo: str + sector: str + country: str + fullTimeEmployees: str + phone: str + address: str + city: str + state: str + zip: str + dcfDiff: float + dcf: float + image: str + ipoDate: str + defaultImage: bool + isEtf: bool + isActivelyTrading: bool + isAdr: bool + isFund: bool + + +class FMPCompanyProfileFetcher( + Fetcher[ # type: ignore + CompanyOverviewQueryParams, + CompanyOverviewData, + FMPCompanyOverviewQueryParams, + FMPCompanyOverviewData, + ] +): + @staticmethod + def transform_query( + query: CompanyOverviewQueryParams, extra_params: Optional[Dict] = None + ) -> FMPCompanyOverviewQueryParams: + return FMPCompanyOverviewQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPCompanyOverviewQueryParams, api_key: str + ) -> FMPCompanyOverviewData: + base_url = "https://financialmodelingprep.com/api/v3/" + request_url = f"{base_url}profile/{query.symbol}?apikey={api_key}" + data = get_data(request_url) + if isinstance(data, dict): + raise ValueError("Expected list of dicts, got dict") + + return FMPCompanyOverviewData.parse_obj(data[0]) + + @staticmethod + def transform_data( # type: ignore + data: FMPCompanyOverviewData, + ) -> CompanyOverviewData: + # Need to update transform_data to return a non list version of list isn't used + return data_transformer(data, CompanyOverviewData) # type: ignore diff --git a/openbb_providers/builtin_providers/fmp/crypto_eod.py b/openbb_providers/builtin_providers/fmp/crypto_eod.py new file mode 100644 index 00000000000..433ee65e36f --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/crypto_eod.py @@ -0,0 +1,99 @@ +"""FMP Cryptos end of day fetcher.""" + +# IMPORT STANDARD +from datetime import date, datetime +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeInt, validator + +from builtin_providers.fmp.helpers import get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.crypto_eod import CryptoEODData, CryptoEODQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + + +class FMPCryptoEODQueryParams(QueryParams): + """FMP Crypto end of day query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Cryptocurrencies + + Parameter + --------- + symbol : str + The symbol of the company. + start_date : date + The start date of the stock data from which to retrieve the data. + end_date : date + The end date of the stock data up to which to retrieve the data. + timeseries : Optional[int] + The number of days to look back. + serietype : Optional[Literal["line"]] + The type of the series. Only "line" is supported. + """ + + __name__ = "FMPCryptoEODQueryParams" + symbol: str = Field(min_length=1) + serietype: Optional[Literal["line"]] + start_date: date + end_date: date + timeseries: Optional[NonNegativeInt] # Number of days to looks back + + +class FMPCryptoEODData(Data): + __name__ = "FMPCryptoEODData" + date: datetime + open: float + high: float + low: float + close: float + adjClose: float = Field(alias="adj_close") + volume: float + unadjustedVolume: float + change: float + changePercent: float + vwap: float + label: str + changeOverTime: float + + @validator("date", pre=True) + def time_validate(cls, v): # pylint: disable=E0213 + return datetime.strptime(v, "%Y-%m-%d") + + +class FMPCryptoEODFetcher( + Fetcher[ + CryptoEODQueryParams, + CryptoEODData, + FMPCryptoEODQueryParams, + FMPCryptoEODData, + ] +): + @staticmethod + def transform_query( + query: CryptoEODQueryParams, extra_params: Optional[Dict] = None + ) -> FMPCryptoEODQueryParams: + raw_end = query.end_date if query.end_date else datetime.now() + return FMPCryptoEODQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=raw_end, + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: FMPCryptoEODQueryParams, api_key: str + ) -> List[FMPCryptoEODData]: + base_url = "https://financialmodelingprep.com/api/v3/" + query_str = get_querystring(query.dict(), ["symbol"]) + query_str = query_str.replace("start_date", "from").replace("end_date", "to") + url = f"{base_url}historical-price-full/{query.symbol}?{query_str}&apikey={api_key}" + return get_data_many(url, FMPCryptoEODData, "historical") + + @staticmethod + def transform_data(data: List[FMPCryptoEODData]) -> List[CryptoEODData]: + return data_transformer(data, CryptoEODData) diff --git a/openbb_providers/builtin_providers/fmp/crypto_price.py b/openbb_providers/builtin_providers/fmp/crypto_price.py new file mode 100644 index 00000000000..fa68ff87b43 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/crypto_price.py @@ -0,0 +1,80 @@ +"""FMP Crypto Price fetcher.""" + +# IMPORT STANDARD +from enum import Enum +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.fmp.helpers import BaseStockPriceData, create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import QueryParams +from openbb_provider.model.data.crypto_price import ( + CryptoPriceData, + CryptoPriceQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class Interval(str, Enum): + oneminute = "1min" + fiveminute = "5min" + fifteenminute = "15min" + thirtyminute = "30min" + onehour = "1hour" + fourhour = "4hour" + + +class FMPCryptoPriceQueryParams(QueryParams): + """FMP Crypto Price query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Cryptocurrencies + + Parameter + --------- + symbol : str + The symbol of the company. + interval : Enum of ["1min", "5min", "15min", "30min", "1hour", "4hour"] (default: "1hour") + The interval of the data. + """ + + __name__ = "FMPCryptoPriceQueryParams" + symbol: str = Field(min_length=1) + interval: Interval = Field(default=Interval.onehour) + + +class FMPCryptoPriceData(BaseStockPriceData): + __name__ = "FMPCryptoPriceData" + + +class FMPCryptoPriceFetcher( + Fetcher[ + CryptoPriceQueryParams, + CryptoPriceData, + FMPCryptoPriceQueryParams, + FMPCryptoPriceData, + ] +): + @staticmethod + def transform_query( + query: CryptoPriceQueryParams, extra_params: Optional[Dict] = None + ) -> FMPCryptoPriceQueryParams: + return FMPCryptoPriceQueryParams( + symbol=query.symbol, **extra_params if extra_params else {} + ) + + @staticmethod + def extract_data( + query: FMPCryptoPriceQueryParams, api_key: str + ) -> List[FMPCryptoPriceData]: + url = create_url( + 3, f"historical-chart/{query.interval}/{query.symbol}", api_key + ) + return get_data_many(url, FMPCryptoPriceData) + + @staticmethod + def transform_data(data: List[FMPCryptoPriceData]) -> List[CryptoPriceData]: + return data_transformer(data, CryptoPriceData) diff --git a/openbb_providers/builtin_providers/fmp/earnings_calendar.py b/openbb_providers/builtin_providers/fmp/earnings_calendar.py new file mode 100644 index 00000000000..0d27540e378 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/earnings_calendar.py @@ -0,0 +1,76 @@ +"""FMP Earnings Calendar fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many +from openbb_provider.model.abstract.data import Data + +# IMPORT INTERNAL +from openbb_provider.model.data.earnings_calendar import ( + EarningsCalendarData, + EarningsCalendarQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPEarningsCalendarQueryParams(EarningsCalendarQueryParams): + """FMP Earnings Calendar query. + + Source: https://site.financialmodelingprep.com/developer/docs/earnings-calendar-api/ + + Parameter + --------- + symbol : str + The symbol of the company. + limit : int + """ + + __name__ = "FMPEarningsCalendarQueryParams" + + +class FMPEarningsCalendarData(Data): + __name__ = "FMPEarningsCalendarData" + + date: dateType + symbol: str + eps: Optional[float] + epsEstimated: Optional[float] + time: str + revenue: Optional[int] + revenueEstimated: Optional[int] + updatedFromDate: Optional[dateType] + fiscalDateEnding: dateType + + +class FMPEarningsCalendarFetcher( + Fetcher[ + EarningsCalendarQueryParams, + EarningsCalendarData, + FMPEarningsCalendarQueryParams, + FMPEarningsCalendarData, + ] +): + @staticmethod + def transform_query( + query: EarningsCalendarQueryParams, extra_params: Optional[Dict] = None + ) -> FMPEarningsCalendarQueryParams: + return FMPEarningsCalendarQueryParams(symbol=query.symbol, limit=query.limit) + + @staticmethod + def extract_data( + query: FMPEarningsCalendarQueryParams, api_key: str + ) -> List[FMPEarningsCalendarData]: + url = create_url( + 3, f"historical/earning_calendar/{query.symbol}", api_key, query, ["symbol"] + ) + return get_data_many(url, FMPEarningsCalendarData) + + @staticmethod + def transform_data( + data: List[FMPEarningsCalendarData], + ) -> List[EarningsCalendarData]: + return data_transformer(data, EarningsCalendarData) diff --git a/openbb_providers/builtin_providers/fmp/earnings_call_transcript.py b/openbb_providers/builtin_providers/fmp/earnings_call_transcript.py new file mode 100644 index 00000000000..1cbcfde994c --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/earnings_call_transcript.py @@ -0,0 +1,67 @@ +"""FMP Earnings Call Transcript fetcher.""" + +# IMPORT STANDARD +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.data.earnings_call_transcript import ( + EarningsCallTranscriptData, + EarningsCallTranscriptQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPEarningsCallTranscriptQueryParams(EarningsCallTranscriptQueryParams): + """FMP Earnings Calendar query. + + Source: https://site.financialmodelingprep.com/developer/docs/earnings-calendar-api/ + + Parameter + --------- + symbol : str + The symbol of the company. + """ + + __name__ = "FMPEarningsCallTranscriptQueryParams" + + +class FMPEarningsCallTranscriptData(EarningsCallTranscriptData): + __name__ = "FMPEarningsCallTranscriptData" + + +class FMPEarningsCallTranscriptFetcher( + Fetcher[ + EarningsCallTranscriptQueryParams, + EarningsCallTranscriptData, + FMPEarningsCallTranscriptQueryParams, + FMPEarningsCallTranscriptData, + ] +): + @staticmethod + def transform_query( + query: EarningsCallTranscriptQueryParams, extra_params: Optional[Dict] = None + ) -> FMPEarningsCallTranscriptQueryParams: + return FMPEarningsCallTranscriptQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPEarningsCallTranscriptQueryParams, api_key: str + ) -> List[FMPEarningsCallTranscriptData]: + url = create_url( + 4, + f"batch_earning_call_transcript/{query.symbol}", + api_key, + query, + ["symbol"], + ) + return get_data_many(url, FMPEarningsCallTranscriptData) + + @staticmethod + def transform_data( + data: List[FMPEarningsCallTranscriptData], + ) -> List[EarningsCallTranscriptData]: + return data_transformer(data, EarningsCallTranscriptData) diff --git a/openbb_providers/builtin_providers/fmp/esg_risk_rating.py b/openbb_providers/builtin_providers/fmp/esg_risk_rating.py new file mode 100644 index 00000000000..087d12cb33d --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/esg_risk_rating.py @@ -0,0 +1,78 @@ +"""FMP ESG Risk Rating fetcher.""" + +# IMPORT STANDARD +from datetime import date +from typing import Dict, List, Literal, Optional + +from pydantic import Field + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data +from openbb_provider.model.data.esg_risk_rating import ( + ESGRiskRatingData, + ESGRiskRatingQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + +current_year = date.today().year +ratings = Literal[ + "A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "D-", "F" +] + + +class FMPESGRiskRatingQueryParams(ESGRiskRatingQueryParams): + """FMP ESG Risk Rating query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Company-ESG-Risk-Ratings + + Parameter + --------- + symbol : str + The symbol of the company. + """ + + __name__ = "FMPESGRiskRatingQueryParams" + + +class FMPESGRiskRatingData(Data): + __name__ = "FMPESGRiskRatingData" + + symbol: str + cik: int + companyName: str + industry: str + year: int + ESGRiskRating: ratings = Field(alias="esg_risk_rating") + industryRank: str + + +class FMPESGRiskRatingFetcher( + Fetcher[ + ESGRiskRatingQueryParams, + ESGRiskRatingData, + FMPESGRiskRatingQueryParams, + FMPESGRiskRatingData, + ] +): + @staticmethod + def transform_query( + query: ESGRiskRatingQueryParams, extra_params: Optional[Dict] = None + ) -> FMPESGRiskRatingQueryParams: + return FMPESGRiskRatingQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPESGRiskRatingQueryParams, api_key: str + ) -> List[FMPESGRiskRatingData]: + url = create_url( + 4, "esg-environmental-social-governance-data-ratings", api_key, query + ) + return get_data_many(url, FMPESGRiskRatingData) + + @staticmethod + def transform_data(data: List[FMPESGRiskRatingData]) -> List[ESGRiskRatingData]: + return data_transformer(data, ESGRiskRatingData) diff --git a/openbb_providers/builtin_providers/fmp/esg_score.py b/openbb_providers/builtin_providers/fmp/esg_score.py new file mode 100644 index 00000000000..aafac8bb2f0 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/esg_score.py @@ -0,0 +1,66 @@ +"""FMP ESG Score fetcher.""" + +# IMPORT STANDARD +from datetime import date, datetime +from typing import Dict, List, Optional + +from pydantic import Field + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data +from openbb_provider.model.data.esg_score import ESGScoreData, ESGScoreQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPESGScoreQueryParams(ESGScoreQueryParams): + """FMP ESG Score query. + + Source: https://site.financialmodelingprep.com/developer/docs/#ESG-SCORE + + Parameter + --------- + symbol : str + The symbol of the company. + """ + + __name__ = "FMPESGScoreQueryParams" + + +class FMPESGScoreData(Data): + __name__ = "FMPESGScoreData" + symbol: str + cik: int + companyName: str + formType: str + acceptedDate: datetime + date: date + environmentalScore: float + socialScore: float + governanceScore: float + ESGScore: float = Field(alias="esg_score") + url: str + + +class FMPESGScoreFetcher( + Fetcher[ESGScoreQueryParams, ESGScoreData, FMPESGScoreQueryParams, FMPESGScoreData] +): + @staticmethod + def transform_query( + query: ESGScoreQueryParams, extra_params: Optional[Dict] = None + ) -> FMPESGScoreQueryParams: + return FMPESGScoreQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPESGScoreQueryParams, api_key: str + ) -> List[FMPESGScoreData]: + url = create_url(4, "esg-environmental-social-governance-data", api_key, query) + return get_data_many(url, FMPESGScoreData) + + @staticmethod + def transform_data(data: List[FMPESGScoreData]) -> List[ESGScoreData]: + return data_transformer(data, ESGScoreData) diff --git a/openbb_providers/builtin_providers/fmp/esg_sector.py b/openbb_providers/builtin_providers/fmp/esg_sector.py new file mode 100644 index 00000000000..fd9d6b23a31 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/esg_sector.py @@ -0,0 +1,69 @@ +"""FMP ESG Sector fetcher.""" + +# IMPORT STANDARD +from datetime import date +from typing import Dict, List, Optional + +from pydantic import Field + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.esg_sector import ESGSectorData, ESGSectorQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + +current_year = date.today().year + + +class FMPESGSectorQueryParams(QueryParams): + """FMP ESG Sector query. + + Source: https://site.financialmodelingprep.com/developer/docs/#ESG-Benchmarking-By-Sector-and-Year + + Parameter + --------- + year : int + The year of the data. + """ + + __name__ = "FMPESGSectorQueryParams" + + year: int = Field(ge=2000, le=current_year) + + +class FMPESGSectorData(Data): + __name__ = "FMPESGSectorData" + year: int + sector: str + environmentalScore: float + socialScore: float + governanceScore: float + ESGScore: float = Field(alias="esg_score") + + +class FMPESGSectorFetcher( + Fetcher[ + ESGSectorQueryParams, ESGSectorData, FMPESGSectorQueryParams, FMPESGSectorData + ] +): + @staticmethod + def transform_query( + query: ESGSectorQueryParams, extra_params: Optional[Dict] = None + ) -> FMPESGSectorQueryParams: + return FMPESGSectorQueryParams(year=query.year) + + @staticmethod + def extract_data( + query: FMPESGSectorQueryParams, api_key: str + ) -> List[FMPESGSectorData]: + url = create_url( + 4, "esg-environmental-social-governance-sector-benchmark", api_key, query + ) + return get_data_many(url, FMPESGSectorData) + + @staticmethod + def transform_data(data: List[FMPESGSectorData]) -> List[ESGSectorData]: + return data_transformer(data, ESGSectorData) diff --git a/openbb_providers/builtin_providers/fmp/executive_compensation.py b/openbb_providers/builtin_providers/fmp/executive_compensation.py new file mode 100644 index 00000000000..152b5ef1ef1 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/executive_compensation.py @@ -0,0 +1,90 @@ +"""FMP Executive Compensation Fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import validator + +from builtin_providers.fmp.helpers import create_url, get_data_many +from openbb_provider.model.abstract.data import Data + +# IMPORT INTERNAL +from openbb_provider.model.data.executive_compensation import ( + ExecutiveCompensationData, + ExecutiveCompensationQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + +# This part is only provided by FMP and not by the other providers for now. + + +class FMPExecutiveCompensationQueryParams(ExecutiveCompensationQueryParams): + """FMP Executive Compensation query. + + Source: https://site.financialmodelingprep.com/developer/docs/executive-compensation-api/ + + Parameter + --------- + symbol : str + The symbol of the company. + """ + + __name__ = "FMPExecutiveCompensationQueryParams" + + +class FMPExecutiveCompensationData(Data): + __name__ = "FMPExecutiveCompensationData" + + cik: Optional[str] + symbol: str + filingDate: dateType + acceptedDate: dateType + nameAndPosition: str + year: int + salary: float + bonus: float + stock_award: float + incentive_plan_compensation: float + all_other_compensation: float + total: float + url: str + + @validator("acceptedDate", pre=True) + def convert_accepted_date(cls, v): # pylint: disable=E0213 + return datetime.strptime(v, "%Y-%m-%d %H:%M:%S") + + @validator("filingDate", pre=True) + def convert_filing_date(cls, v): # pylint: disable=E0213 + return datetime.strptime(v, "%Y-%m-%d") + + +class FMPExecutiveCompensationFetcher( + Fetcher[ # type: ignore + ExecutiveCompensationQueryParams, + ExecutiveCompensationData, + FMPExecutiveCompensationQueryParams, + FMPExecutiveCompensationData, + ] +): + @staticmethod + def transform_query( + query: ExecutiveCompensationQueryParams, extra_params: Optional[Dict] = None + ) -> FMPExecutiveCompensationQueryParams: + return FMPExecutiveCompensationQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPExecutiveCompensationQueryParams, api_key: str + ) -> List[FMPExecutiveCompensationData]: + url = create_url(4, "governance/executive_compensation", api_key, query) + return get_data_many(url, FMPExecutiveCompensationData) + + @staticmethod + def transform_data( + data: List[FMPExecutiveCompensationData], + ) -> List[ExecutiveCompensationData]: + return data_transformer(data, ExecutiveCompensationData) diff --git a/openbb_providers/builtin_providers/fmp/executives.py b/openbb_providers/builtin_providers/fmp/executives.py new file mode 100644 index 00000000000..9207af87055 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/executives.py @@ -0,0 +1,109 @@ +"""FMP Key Executives Fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import get_data + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.base import BaseSymbol +from openbb_provider.model.data.key_executives import ( + KeyExecutivesData, + KeyExecutivesQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPKeyExecutivesQueryParams(QueryParams, BaseSymbol): + """FMP Key Executives QueryParams. + + Source: https://financialmodelingprep.com/developer/docs/#Key-Executives + + Parameter + --------- + symbol : str + The symbol of the company. + key_executive_name : Optional[str] + The name of the key executive. + key_executive_title : Optional[str] + The title of the key executive. + key_executive_title_since : Optional[datetime] + The title since of the key executive. + key_executive_year_born : Optional[datetime] + The year born of the key executive. + key_executive_gender: Optional[str] + The gender of the key executive. + """ + + __name__ = "FMPKeyExecutivesQueryParams" + key_executive_name: Optional[str] + key_executive_title: Optional[str] + key_executive_title_since: Optional[datetime] + key_executive_year_born: Optional[datetime] + key_executive_gender: Optional[str] + + +class FMPKeyExecutivesData(Data): + __name__ = "FMPKeyExecutivesData" + name: str + title: str + titleSince: Optional[datetime] + yearBorn: Optional[datetime] + gender: Optional[str] + + +class FMPKeyExecutivesFetcher( + Fetcher[ + KeyExecutivesQueryParams, + KeyExecutivesData, + FMPKeyExecutivesQueryParams, + FMPKeyExecutivesData, + ] +): + @staticmethod + def transform_query( + query: KeyExecutivesQueryParams, extra_params: Optional[Dict] = None + ) -> FMPKeyExecutivesQueryParams: + return FMPKeyExecutivesQueryParams( + symbol=query.symbol, + key_executive_name=query.key_executive_name, + key_executive_title=query.key_executive_title, + key_executive_title_since=query.key_executive_title_since, + key_executive_year_born=query.key_executive_year_born, + key_executive_gender=query.key_executive_gender, + ) + + @staticmethod + def extract_data( + query: FMPKeyExecutivesQueryParams, api_key: str + ) -> List[FMPKeyExecutivesData]: + base_url = "https://financialmodelingprep.com/api/v3/" + request_url = f"{base_url}key-executives/{query.symbol}?apikey={api_key}" + data = get_data(request_url) + if isinstance(data, dict): + raise ValueError("Expected list of dicts, got dict") + + if query.key_executive_name: + data = [x for x in data if x["name"] == query.key_executive_name] + if query.key_executive_title: + data = [x for x in data if x["title"] == query.key_executive_title] + if query.key_executive_title_since: + data = [ + x for x in data if x["titleSince"] == query.key_executive_title_since + ] + if query.key_executive_year_born: + data = [x for x in data if x["yearBorn"] == query.key_executive_year_born] + if query.key_executive_gender: + data = [x for x in data if x["gender"] == query.key_executive_gender] + + return [FMPKeyExecutivesData(**d) for d in data] + + @staticmethod + def transform_data( + data: List[FMPKeyExecutivesData], + ) -> List[KeyExecutivesData]: + return data_transformer(data, KeyExecutivesData) diff --git a/openbb_providers/builtin_providers/fmp/forex_eod.py b/openbb_providers/builtin_providers/fmp/forex_eod.py new file mode 100644 index 00000000000..3a7c5821b77 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/forex_eod.py @@ -0,0 +1,98 @@ +"""FMP Forex end of day fetcher.""" + +# IMPORT STANDARD +from datetime import date, datetime +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeInt, validator + +from builtin_providers.fmp.helpers import get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.forex_eod import ForexEODData, ForexEODQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + + +class FMPForexEODQueryParams(QueryParams): + """FMP Forex end of day query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Historical-Forex-Price + + Parameter + --------- + symbol : str + The symbol of the company. + start_date : date + The start date of the stock data from which to retrieve the data. + end_date : date + The end date of the stock data up to which to retrieve the data. + timeseries : Optional[int] + The number of days to look back. + serietype : Optional[Literal["line"]] + The type of the series. Only "line" is supported. + """ + + __name__ = "FMPForexEODQueryParams" + symbol: str = Field(min_length=1) + serietype: Optional[Literal["line"]] = None + start_date: date + end_date: date + timeseries: Optional[NonNegativeInt] = None # Number of days to looks back + + +class FMPForexEODData(Data): + __name__ = "FMPForexEODData" + date: datetime + open: float + high: float + low: float + close: float + adjClose: float = Field(alias="adj_close") + volume: float + unadjustedVolume: float + change: float + changePercent: float + vwap: float + label: str + changeOverTime: float + + @validator("date", pre=True) + def time_validate(cls, v): # pylint: disable=E0213 + return datetime.strptime(v, "%Y-%m-%d") + + +class FMPForexEODFetcher( + Fetcher[ + ForexEODQueryParams, + ForexEODData, + FMPForexEODQueryParams, + FMPForexEODData, + ] +): + @staticmethod + def transform_query( + query: ForexEODQueryParams, extra_params: Optional[Dict] = None + ) -> FMPForexEODQueryParams: + raw_end = query.end_date if query.end_date else datetime.now() + return FMPForexEODQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=raw_end, + ) + + @staticmethod + def extract_data( + query: FMPForexEODQueryParams, api_key: str + ) -> List[FMPForexEODData]: + base_url = "https://financialmodelingprep.com/api/v3/" + query_str = get_querystring(query.dict(), ["symbol"]) + query_str = query_str.replace("start_date", "from").replace("end_date", "to") + url = f"{base_url}historical-price-full/{query.symbol}?{query_str}&apikey={api_key}" + return get_data_many(url, FMPForexEODData, "historical") + + @staticmethod + def transform_data(data: List[FMPForexEODData]) -> List[ForexEODData]: + return data_transformer(data, ForexEODData) diff --git a/openbb_providers/builtin_providers/fmp/forex_price.py b/openbb_providers/builtin_providers/fmp/forex_price.py new file mode 100644 index 00000000000..a3144a8cb5b --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/forex_price.py @@ -0,0 +1,77 @@ +"""FMP Forex Price fetcher.""" + +# IMPORT STANDARD +from enum import Enum +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.fmp.helpers import BaseStockPriceData, create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import QueryParams +from openbb_provider.model.data.forex_price import ForexPriceData, ForexPriceQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class Interval(str, Enum): + oneminute = "1min" + fiveminute = "5min" + fifteenminute = "15min" + thirtyminute = "30min" + onehour = "1hour" + fourhour = "4hour" + + +class FMPForexPriceQueryParams(QueryParams): + """FMP Forex Price query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Forex-Historical-Price + + Parameter + --------- + symbol : str + The symbol of the company. + interval : Enum of ["1min", "5min", "15min", "30min", "1hour", "4hour"] (default: "1hour") + The interval of the data. + """ + + __name__ = "FMPForexPriceQueryParams" + symbol: str = Field(min_length=1) + interval: Literal["1min", "5min", "15min", "30min", "1hour", "4hour"] = "1hour" + + +class FMPForexPriceData(BaseStockPriceData): + __name__ = "FMPForexPriceData" + + +class FMPForexPriceFetcher( + Fetcher[ + ForexPriceQueryParams, + ForexPriceData, + FMPForexPriceQueryParams, + FMPForexPriceData, + ] +): + @staticmethod + def transform_query( + query: ForexPriceQueryParams, extra_params: Optional[Dict] = None + ) -> FMPForexPriceQueryParams: + return FMPForexPriceQueryParams( + symbol=query.symbol, **extra_params if extra_params else {} + ) + + @staticmethod + def extract_data( + query: FMPForexPriceQueryParams, api_key: str + ) -> List[FMPForexPriceData]: + url = create_url( + 3, f"historical-chart/{query.interval}/{query.symbol}", api_key + ) + return get_data_many(url, FMPForexPriceData) + + @staticmethod + def transform_data(data: List[FMPForexPriceData]) -> List[ForexPriceData]: + return data_transformer(data, ForexPriceData) diff --git a/openbb_providers/builtin_providers/fmp/global_news.py b/openbb_providers/builtin_providers/fmp/global_news.py new file mode 100644 index 00000000000..f57a572bca4 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/global_news.py @@ -0,0 +1,67 @@ +"""FMP Global News fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.global_news import GlobalNewsData, GlobalNewsQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPGlobalNewsQueryParams(QueryParams): + """FMP Global News query. + + Source: https://site.financialmodelingprep.com/developer/docs/general-news-api/ + + Parameter + --------- + page : int (default: 0) + The page of the data to retrieve. + """ + + __name__ = "FMPGlobalNewsQueryParams" + page: int = Field(default=0) + + +class FMPGlobalNewsData(Data): + __name__ = "FMPGlobalNewsData" + publishedDate: datetime = Field(alias="date") + title: str + image: str + text: str + url: str + site: str + + +class FMPGlobalNewsFetcher( + Fetcher[ + GlobalNewsQueryParams, + GlobalNewsData, + FMPGlobalNewsQueryParams, + FMPGlobalNewsData, + ] +): + @staticmethod + def transform_query( + query: GlobalNewsQueryParams, extra_params: Optional[Dict] = None + ) -> FMPGlobalNewsQueryParams: + return FMPGlobalNewsQueryParams(page=query.page) + + @staticmethod + def extract_data( + query: FMPGlobalNewsQueryParams, api_key: str + ) -> List[FMPGlobalNewsData]: + url = create_url(4, "general_news", api_key, query) + return get_data_many(url, FMPGlobalNewsData) + + @staticmethod + def transform_data(data: List[FMPGlobalNewsData]) -> List[GlobalNewsData]: + return data_transformer(data, GlobalNewsData) diff --git a/openbb_providers/builtin_providers/fmp/helpers.py b/openbb_providers/builtin_providers/fmp/helpers.py new file mode 100644 index 00000000000..ee63a775163 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/helpers.py @@ -0,0 +1,123 @@ +"""FMP Helpers Module.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import List, Optional, Type, TypeVar, Union + +# IMPORT THIRD-PARTY +import requests +from pydantic import BaseModel, NonNegativeInt, PositiveFloat, validator +from requests.exceptions import SSLError + +from openbb_provider.model.abstract.data import Data + +# IMPORT INTERNAL +from openbb_provider.provider.abstract.fetcher import QueryParamsType +from openbb_provider.provider.provider_helpers import ( + BasicResponse, + get_querystring, + request, +) + +T = TypeVar("T", bound=BaseModel) + + +def get_data(url: str) -> Union[list, dict]: + try: + r: Union[requests.Response, BasicResponse] = requests.get(url, timeout=10) + except SSLError: + r = request(url) + if r.status_code == 404: + raise RuntimeError("FMP endpoint doesn't exist") + + data = r.json() + if r.status_code != 200: + message = data.get("message", "unknown error") + raise RuntimeError(f"Error in FMP request: {message}") + + if "Error Message" in data: + raise RuntimeError("FMP Error Message: " + data["Error Message"]) + + if len(data) == 0: + raise RuntimeError("No results found. Try adjusting the query parameters.") + + return data + + +def create_url( + version: int, + endpoint: str, + api_key: str, + query: Optional[QueryParamsType] = None, + exclude: Optional[List[str]] = None, +) -> str: + """Creates a URL for the FMP API. + + Parameters: + ----------- + version: int + The version of the API to use. + endpoint: str + The endpoint to use. + api_key: str + The API key to use. + query: Optional[QueryParamsType] + The dictionary to be turned into a querystring. + exclude: List[str] + The keys to be excluded from the querystring. + + Returns: + -------- + str + The querystring. + + """ + the_dict = {} if not query else query.dict() + query_string = get_querystring(the_dict, exclude or []) + base_url = f"https://financialmodelingprep.com/api/v{version}/" + return f"{base_url}{endpoint}?{query_string}&apikey={api_key}" + + +def get_data_many( + url: str, to_schema: Type[T], sub_dict: Optional[str] = None +) -> List[T]: + """Get data from FMP endpoint and convert to list of schemas. + + Parameters: + ----------- + url: str + The URL to get the data from. + to_schema: T + The schema to convert the data to. + sub_dict: Optional[str] + The sub-dictionary to use. + """ + data = get_data(url) + if sub_dict and isinstance(data, dict): + data = data.get(sub_dict, []) + if isinstance(data, dict): + raise ValueError("Expected list of dicts, got dict") + return [to_schema(**d) for d in data] + + +def get_data_one(url: str, to_schema: Type[T]) -> T: + """Get data from FMP endpoint and convert to schema.""" + data = get_data(url) + if isinstance(data, list): + raise ValueError("Expected dict with lists, got list") + return to_schema(**data) + + +class BaseStockPriceData(Data): + open: PositiveFloat + high: PositiveFloat + low: PositiveFloat + close: PositiveFloat + volume: NonNegativeInt + date: datetime + + @validator("date", pre=True) + def time_validate(cls, v: str) -> datetime: # pylint: disable=E0213 + if len(v) < 12: + return datetime.strptime(v, "%Y-%m-%d") + return datetime.strptime(v, "%Y-%m-%d %H:%M:%S") diff --git a/openbb_providers/builtin_providers/fmp/historical_dividends.py b/openbb_providers/builtin_providers/fmp/historical_dividends.py new file mode 100644 index 00000000000..f598d1f065b --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/historical_dividends.py @@ -0,0 +1,93 @@ +"""FMP Historical Dividends fetcher.""" +# IMPORT STANDARD +from datetime import date as dateType +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import validator + +from builtin_providers.fmp.helpers import create_url, get_data_many +from openbb_provider.model.abstract.data import Data + +# IMPORT INTERNAL +from openbb_provider.model.data.historical_dividends import ( + HistoricalDividendsData, + HistoricalDividendsQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPHistoricalDividendsQueryParams(HistoricalDividendsQueryParams): + """FMP Earnings Calendar query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Historical-Dividends + + Parameter + --------- + symbol : str + The symbol of the company. + """ + + __name__ = "FMPHistoricalDividendsQueryParams" + + +class FMPHistoricalDividendsData(Data): + __name__ = "FMPHistoricalDividendsData" + + date: dateType + label: str + adjDividend: float + dividend: float + recordDate: Optional[dateType] + paymentDate: Optional[dateType] + declarationDate: Optional[dateType] + + @validator("declarationDate", pre=True) + def declaration_date_validate(cls, v: str): # pylint: disable=E0213 + if v: + return datetime.strptime(v, "%Y-%m-%d").date() + return None + + @validator("recordDate", pre=True) + def record_date_validate(cls, v: str): # pylint: disable=E0213 + if v: + return datetime.strptime(v, "%Y-%m-%d").date() + return None + + @validator("paymentDate", pre=True) + def payment_date_validate(cls, v: str): # pylint: disable=E0213 + if v: + return datetime.strptime(v, "%Y-%m-%d").date() + return None + + +class FMPHistoricalDividendsFetcher( + Fetcher[ + HistoricalDividendsQueryParams, + HistoricalDividendsData, + FMPHistoricalDividendsQueryParams, + FMPHistoricalDividendsData, + ] +): + @staticmethod + def transform_query( + query: HistoricalDividendsQueryParams, extra_params: Optional[Dict] = None + ) -> FMPHistoricalDividendsQueryParams: + return FMPHistoricalDividendsQueryParams(symbol=query.symbol) + + @staticmethod + def extract_data( + query: FMPHistoricalDividendsQueryParams, api_key: str + ) -> List[FMPHistoricalDividendsData]: + url = create_url( + 3, f"historical-price-full/stock_dividend/{query.symbol}", api_key + ) + return get_data_many(url, FMPHistoricalDividendsData, "historical") + + @staticmethod + def transform_data( + data: List[FMPHistoricalDividendsData], + ) -> List[HistoricalDividendsData]: + return data_transformer(data, HistoricalDividendsData) diff --git a/openbb_providers/builtin_providers/fmp/historical_stock_splits.py b/openbb_providers/builtin_providers/fmp/historical_stock_splits.py new file mode 100644 index 00000000000..502ff8fdd46 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/historical_stock_splits.py @@ -0,0 +1,63 @@ +"""FMP Historical Stock Splits fetcher.""" + +# IMPORT STANDARD +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.data.historical_stock_splits import ( + HistoricalStockSplitsData, + HistoricalStockSplitsQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPHistoricalStockSplitsQueryParams(HistoricalStockSplitsQueryParams): + """FMP Historical Stock Splits query. + + Source: https://site.financialmodelingprep.com/developer/docs/earnings-calendar-api/ + + Parameter + --------- + symbol : str + The symbol of the company. + """ + + __name__ = "FMPHistoricalStockSplitsQueryParams" + + +class FMPHistoricalStockSplitsData(HistoricalStockSplitsData): + __name__ = "FMPHistoricalStockSplitsData" + + +class FMPHistoricalStockSplitsFetcher( + Fetcher[ + HistoricalStockSplitsQueryParams, + HistoricalStockSplitsData, + FMPHistoricalStockSplitsQueryParams, + FMPHistoricalStockSplitsData, + ] +): + @staticmethod + def transform_query( + query: HistoricalStockSplitsQueryParams, extra_params: Optional[Dict] = None + ) -> FMPHistoricalStockSplitsQueryParams: + return FMPHistoricalStockSplitsQueryParams(symbol=query.symbol) + + @staticmethod + def extract_data( + query: FMPHistoricalStockSplitsQueryParams, api_key: str + ) -> List[FMPHistoricalStockSplitsData]: + url = create_url( + 3, f"historical-price-full/stock_split/{query.symbol}", api_key + ) + return get_data_many(url, FMPHistoricalStockSplitsData, "historical") + + @staticmethod + def transform_data( + data: List[FMPHistoricalStockSplitsData], + ) -> List[HistoricalStockSplitsData]: + return data_transformer(data, HistoricalStockSplitsData) diff --git a/openbb_providers/builtin_providers/fmp/income_statement.py b/openbb_providers/builtin_providers/fmp/income_statement.py new file mode 100644 index 00000000000..b35d57bd741 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/income_statement.py @@ -0,0 +1,131 @@ +"""FMP Income Statement Fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from datetime import datetime +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeInt, root_validator + +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.income_statement import ( + IncomeStatementData, + IncomeStatementQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + +PeriodType = Literal["annual", "quarter"] + + +class FMPIncomeStatementQueryParams(QueryParams): + """FMP Income Statement QueryParams. + + Source: https://financialmodelingprep.com/developer/docs/#Income-Statement + + Parameter + --------- + symbol : Optional[str] + The symbol of the company if no CIK is provided. + cik : Optional[str] + The CIK of the company if no symbol is provided. + period : Literal["annual", "quarter"] + The period of the income statement. Default is "annual". + limit : Optional[NonNegativeInt] + The limit of the income statement. + """ + + __name__ = "FMPIncomeStatementQueryParams" + symbol: Optional[str] + cik: Optional[str] + period: PeriodType = Field(default="annual") + limit: Optional[NonNegativeInt] + + @root_validator() + def check_symbol_or_cik(cls, values): # pylint: disable=no-self-argument + if values.get("symbol") is None and values.get("cik") is None: + raise ValueError("symbol or cik must be provided") + return values + + +class FMPIncomeStatementData(Data): + __name__ = "FMPIncomeStatementData" + date: dateType + symbol: str + reportedCurrency: str = Field(alias="currency") + cik: Optional[int] + fillingDate: Optional[dateType] = Field(alias="filing_date") + acceptedDate: Optional[datetime] + calendarYear: Optional[int] + period: Optional[str] + revenue: Optional[int] + costOfRevenue: Optional[int] + grossProfit: Optional[int] + grossProfitRatio: Optional[float] + researchAndDevelopmentExpenses: Optional[int] + generalAndAdministrativeExpenses: Optional[int] + sellingAndMarketingExpenses: Optional[int] + sellingGeneralAndAdministrativeExpenses: Optional[int] + otherExpenses: Optional[int] + operatingExpenses: Optional[int] + costAndExpenses: Optional[int] + interestIncome: Optional[int] + interestExpense: Optional[int] + depreciationAndAmortization: Optional[int] + ebitda: Optional[int] + ebitdaratio: Optional[float] + operatingIncome: Optional[int] + operatingIncomeRatio: Optional[float] + totalOtherIncomeExpensesNet: Optional[int] + incomeBeforeTax: Optional[int] + incomeBeforeTaxRatio: Optional[float] + incomeTaxExpense: Optional[int] + netIncome: Optional[int] + netIncomeRatio: Optional[float] + eps: Optional[float] + epsdiluted: Optional[float] + weightedAverageShsOut: Optional[int] = Field( + alias="weighted_average_shares_outstanding" + ) + weightedAverageShsOutDil: Optional[int] = Field( + alias="weighted_average_shares_outstanding_dil" + ) + link: Optional[str] + finalLink: Optional[str] + + +class FMPIncomeStatementFetcher( + Fetcher[ + IncomeStatementQueryParams, + IncomeStatementData, + FMPIncomeStatementQueryParams, + FMPIncomeStatementData, + ] +): + @staticmethod + def transform_query( + query: IncomeStatementQueryParams, extra_params: Optional[Dict] = None + ) -> FMPIncomeStatementQueryParams: + period: PeriodType = "annual" if query.period == "annually" else "quarter" + return FMPIncomeStatementQueryParams( + symbol=query.symbol, period=period, **extra_params if extra_params else {} + ) + + @staticmethod + def extract_data( + query: FMPIncomeStatementQueryParams, api_key: str + ) -> List[FMPIncomeStatementData]: + url = create_url( + 3, f"income-statement/{query.symbol}", api_key, query, ["symbol"] + ) + return get_data_many(url, FMPIncomeStatementData) + + @staticmethod + def transform_data( + data: List[FMPIncomeStatementData], + ) -> List[IncomeStatementData]: + return data_transformer(data, IncomeStatementData) diff --git a/openbb_providers/builtin_providers/fmp/institutional_ownership.py b/openbb_providers/builtin_providers/fmp/institutional_ownership.py new file mode 100644 index 00000000000..d8c06d4501e --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/institutional_ownership.py @@ -0,0 +1,118 @@ +"""FMP Institutional Ownership Fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, validator + +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.base import BaseSymbol +from openbb_provider.model.data.institutional_ownership import ( + InstitutionalOwnershipData, + InstitutionalOwnershipQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPInstitutionalOwnershipQueryParams(QueryParams, BaseSymbol): + """FMP Institutional Ownership QueryParams. + + Source: https://site.financialmodelingprep.com/developer/docs/institutional-stock-ownership-api/ + + Parameter + --------- + symbol : str + The symbol of the company if cik is not provided. + include_current_quarter : bool + Whether to include the current quarter. Default is False. + date : Optional[dateType] + A specific date to get data for. + """ + + __name__ = "FMPInstitutionalOwnershipQueryParams" + includeCurrentQuarter: bool = Field(default=False, alias="include_current_quarter") + date: Optional[dateType] + + +class FMPInstitutionalOwnershipData(Data): + __name__ = "FMPInstitutionalOwnershipData" + symbol: str = Field(min_length=1) + cik: str = Field(min_length=1) + date: dateType + investorsHolding: int + lastInvestorsHolding: int + investorsHoldingChange: int + numberOf13Fshares: int + lastNumberOf13Fshares: int + numberOf13FsharesChange: int + totalInvested: float + lastTotalInvested: float + totalInvestedChange: float + ownershipPercent: float + lastOwnershipPercent: float + ownershipPercentChange: float + newPositions: int + lastNewPositions: int + newPositionsChange: int + increasedPositions: int + lastIncreasedPositions: int + increasedPositionsChange: int + closedPositions: int + lastClosedPositions: int + closedPositionsChange: int + reducedPositions: int + lastReducedPositions: int + reducedPositionsChange: int + totalCalls: int + lastTotalCalls: int + totalCallsChange: int + totalPuts: int + lastTotalPuts: int + totalPutsChange: int + putCallRatio: float + lastPutCallRatio: float + putCallRatioChange: float + + @validator("date", pre=True) + def time_validate(cls, v): # pylint: disable=no-self-argument + return datetime.strptime(v, "%Y-%m-%d") + + +class FMPInstitutionalOwnershipFetcher( + Fetcher[ + InstitutionalOwnershipQueryParams, + InstitutionalOwnershipData, + FMPInstitutionalOwnershipQueryParams, + FMPInstitutionalOwnershipData, + ] +): + @staticmethod + def transform_query( + query: InstitutionalOwnershipQueryParams, extra_params: Optional[Dict] = None + ) -> FMPInstitutionalOwnershipQueryParams: + return FMPInstitutionalOwnershipQueryParams( + symbol=query.symbol, + date=query.date, + include_current_quarter=query.include_current_quarter, + **extra_params if extra_params else {} + ) + + @staticmethod + def extract_data( + query: FMPInstitutionalOwnershipQueryParams, api_key: str + ) -> List[FMPInstitutionalOwnershipData]: + url = create_url(4, "institutional-ownership/symbol-ownership", api_key, query) + return get_data_many(url, FMPInstitutionalOwnershipData) + + @staticmethod + def transform_data( + data: List[FMPInstitutionalOwnershipData], + ) -> List[InstitutionalOwnershipData]: + return data_transformer(data, InstitutionalOwnershipData) diff --git a/openbb_providers/builtin_providers/fmp/key_metrics.py b/openbb_providers/builtin_providers/fmp/key_metrics.py new file mode 100644 index 00000000000..b130e619b7f --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/key_metrics.py @@ -0,0 +1,139 @@ +"""Key Metrics fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.key_metrics import KeyMetricsData, KeyMetricsQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPKeyMetricsQueryParams(QueryParams): + """FMP Key Metrics QueryParams. + + Source: https://site.financialmodelingprep.com/developer/docs/company-key-metrics-api/ + + Parameter + --------- + symbol : str + The symbol of the company. + period : Literal["quarter", "annual"] + The period of the key metrics. Default is "annual". + limit : Optional[int] + The limit of the key metrics to be returned. + """ + + __name__ = "FMPKeyMetricsQueryParams" + + symbol: str + period: Literal["quarter", "annual"] = "annual" + limit: Optional[int] = None + + +class FMPKeyMetricsData(Data): + __name__ = "FMPKeyMetricsData" + + symbol: str + date: dateType + period: str + revenuePerShare: float + netIncomePerShare: float + operatingCashFlowPerShare: Optional[float] + freeCashFlowPerShare: Optional[float] + cashPerShare: Optional[float] + bookValuePerShare: Optional[float] + tangibleBookValuePerShare: Optional[float] + shareholdersEquityPerShare: Optional[float] + interestDebtPerShare: Optional[float] + marketCap: Optional[float] + enterpriseValue: Optional[float] + peRatio: Optional[float] + priceToSalesRatio: Optional[float] + pocfratio: Optional[float] = Field(alias="pocf_ratio") + pfcfRatio: Optional[float] + pbRatio: Optional[float] + ptbRatio: Optional[float] + evToSales: Optional[float] + enterpriseValueOverEBITDA: Optional[float] = Field( + alias="enterprise_value_over_ebitda" + ) + evToOperatingCashFlow: Optional[float] + evToFreeCashFlow: Optional[float] + earningsYield: Optional[float] + freeCashFlowYield: Optional[Optional[float]] + debtToEquity: Optional[float] + debtToAssets: Optional[float] + netDebtToEBITDA: Optional[float] = Field(alias="net_debt_to_ebitda") + currentRatio: Optional[float] + interestCoverage: Optional[float] + incomeQuality: Optional[float] + dividendYield: Optional[Optional[float]] + payoutRatio: Optional[Optional[float]] + salesGeneralAndAdministrativeToRevenue: Optional[float] + researchAndDdevelopementToRevenue: Optional[float] = Field( + alias="research_and_developement_to_revenue" + ) + intangiblesToTotalAssets: Optional[float] + capexToOperatingCashFlow: Optional[float] + capexToRevenue: Optional[float] + capexToDepreciation: Optional[float] + stockBasedCompensationToRevenue: Optional[float] + grahamNumber: Optional[float] + roic: Optional[float] + returnOnTangibleAssets: Optional[float] + grahamNetNet: Optional[float] + workingCapital: Optional[float] + tangibleAssetValue: Optional[float] + netCurrentAssetValue: Optional[float] + investedCapital: Optional[float] + averageReceivables: Optional[float] + averagePayables: Optional[float] + averageInventory: Optional[float] + daysSalesOutstanding: Optional[float] + daysPayablesOutstanding: Optional[float] + daysOfInventoryOnHand: Optional[float] + receivablesTurnover: Optional[float] + payablesTurnover: Optional[float] + inventoryTurnover: Optional[float] + roe: Optional[float] + capexPerShare: Optional[float] + + +class FMPKeyMetricsFetcher( + Fetcher[ + KeyMetricsQueryParams, + KeyMetricsData, + FMPKeyMetricsQueryParams, + FMPKeyMetricsData, + ] +): + @staticmethod + def transform_query( + query: KeyMetricsQueryParams, extra_params: Optional[Dict] = None + ) -> FMPKeyMetricsQueryParams: + return FMPKeyMetricsQueryParams( + symbol=query.symbol, + limit=query.limit, + period="annual" if query.period == "annually" else "quarter", + ) + + @staticmethod + def extract_data( + query: FMPKeyMetricsQueryParams, api_key: str + ) -> List[FMPKeyMetricsData]: + url = create_url( + 3, f"key-metrics/{query.symbol}", api_key, query, exclude=["symbol"] + ) + return get_data_many(url, FMPKeyMetricsData) + + @staticmethod + def transform_data(data: List[FMPKeyMetricsData]) -> List[KeyMetricsData]: + return data_transformer(data, KeyMetricsData) diff --git a/openbb_providers/builtin_providers/fmp/major_indices_eod.py b/openbb_providers/builtin_providers/fmp/major_indices_eod.py new file mode 100644 index 00000000000..df438887e0b --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/major_indices_eod.py @@ -0,0 +1,102 @@ +"""FMP Major Indices end of day fetcher.""" + +# IMPORT STANDARD +from datetime import date, datetime +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeInt, validator + +from builtin_providers.fmp.helpers import get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.major_indices_eod import ( + MajorIndicesEODData, + MajorIndicesEODQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + + +class FMPMajorIndicesEODQueryParams(QueryParams): + """FMP Major Indices end of day query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Historical-stock-index-prices + + Parameter + --------- + symbol : str + The symbol of the company. + start_date : date + The start date of the stock data from which to retrieve the data. + end_date : date + The end date of the stock data up to which to retrieve the data. + timeseries : Optional[int] + The number of days to look back. + serietype : Optional[Literal["line"]] + The type of the series. Only "line" is supported. + """ + + __name__ = "FMPMajorIndicesEODQueryParams" + symbol: str = Field(min_length=1) + serietype: Optional[Literal["line"]] + start_date: date + end_date: date + timeseries: Optional[NonNegativeInt] # Number of days to looks back + + +class FMPMajorIndicesEODData(Data): + __name__ = "FMPMajorIndicesEODData" + date: datetime + open: float + high: float + low: float + close: float + adjClose: float = Field(alias="adj_close") + volume: float + unadjustedVolume: float + change: float + changePercent: float + vwap: float + label: str + changeOverTime: float + + @validator("date", pre=True) + def time_validate(cls, v): # pylint: disable=E0213 + return datetime.strptime(v, "%Y-%m-%d") + + +class FMPMajorIndicesEODFetcher( + Fetcher[ + MajorIndicesEODQueryParams, + MajorIndicesEODData, + FMPMajorIndicesEODQueryParams, + FMPMajorIndicesEODData, + ] +): + @staticmethod + def transform_query( + query: MajorIndicesEODQueryParams, extra_params: Optional[Dict] = None + ) -> FMPMajorIndicesEODQueryParams: + raw_end = query.end_date if query.end_date else datetime.now() + return FMPMajorIndicesEODQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=raw_end, + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: FMPMajorIndicesEODQueryParams, api_key: str + ) -> List[FMPMajorIndicesEODData]: + base_url = "https://financialmodelingprep.com/api/v3/" + query_str = get_querystring(query.dict(), ["symbol"]) + query_str = query_str.replace("start_date", "from").replace("end_date", "to") + url = f"{base_url}historical-price-full/{query.symbol}?{query_str}&apikey={api_key}" + return get_data_many(url, FMPMajorIndicesEODData, "historical") + + @staticmethod + def transform_data(data: List[FMPMajorIndicesEODData]) -> List[MajorIndicesEODData]: + return data_transformer(data, MajorIndicesEODData) diff --git a/openbb_providers/builtin_providers/fmp/major_indices_price.py b/openbb_providers/builtin_providers/fmp/major_indices_price.py new file mode 100644 index 00000000000..c31369d5129 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/major_indices_price.py @@ -0,0 +1,82 @@ +"""FMP Major Indices Price fetcher.""" + +# IMPORT STANDARD +from enum import Enum +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.fmp.helpers import BaseStockPriceData, create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import QueryParams +from openbb_provider.model.data.major_indices_price import ( + MajorIndicesPriceData, + MajorIndicesPriceQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class Interval(str, Enum): + oneminute = "1min" + fiveminute = "5min" + fifteenminute = "15min" + thirtyminute = "30min" + onehour = "1hour" + fourhour = "4hour" + + +class FMPMajorIndicesPriceQueryParams(QueryParams): + """FMP MajorIndices Price query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Historical-stock-index-prices + + Parameter + --------- + symbol : str + The symbol of the company. + interval : Enum of ["1min", "5min", "15min", "30min", "1hour", "4hour"] (default: "1hour") + The interval of the data. + """ + + __name__ = "FMPMajorIndicesPriceQueryParams" + symbol: str = Field(min_length=1) + interval: Interval = Field(default=Interval.onehour) + + +class FMPMajorIndicesPriceData(BaseStockPriceData): + __name__ = "FMPMajorIndicesPriceData" + + +class FMPMajorIndicesPriceFetcher( + Fetcher[ + MajorIndicesPriceQueryParams, + MajorIndicesPriceData, + FMPMajorIndicesPriceQueryParams, + FMPMajorIndicesPriceData, + ] +): + @staticmethod + def transform_query( + query: MajorIndicesPriceQueryParams, extra_params: Optional[Dict] = None + ) -> FMPMajorIndicesPriceQueryParams: + return FMPMajorIndicesPriceQueryParams( + symbol=query.symbol, **extra_params if extra_params else {} + ) + + @staticmethod + def extract_data( + query: FMPMajorIndicesPriceQueryParams, api_key: str + ) -> List[FMPMajorIndicesPriceData]: + url = create_url( + 3, f"historical-chart/{query.interval}/{query.symbol}", api_key + ) + return get_data_many(url, FMPMajorIndicesPriceData) + + @staticmethod + def transform_data( + data: List[FMPMajorIndicesPriceData], + ) -> List[MajorIndicesPriceData]: + return data_transformer(data, MajorIndicesPriceData) diff --git a/openbb_providers/builtin_providers/fmp/price_target.py b/openbb_providers/builtin_providers/fmp/price_target.py new file mode 100644 index 00000000000..347ac8853fe --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/price_target.py @@ -0,0 +1,75 @@ +"""FMP Stock Ownership fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.fmp.helpers import create_url, get_data_many +from openbb_provider.model.abstract.data import Data + +# IMPORT INTERNAL +from openbb_provider.model.data.price_target import ( + PriceTargetData, + PriceTargetQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPPriceTargetQueryParams(PriceTargetQueryParams): + """FMP Price Target query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Price-Target + + Parameter + --------- + symbol : str + The symbol of the company. + """ + + __name__ = "FMPPriceTargetQueryParams" + + +class FMPPriceTargetData(Data): + __name__ = "FMPPriceTargetData" + + symbol: str + publishedDate: datetime + newsURL: str = Field(alias="news_url") + newsTitle: Optional[str] + analystName: Optional[str] + priceTarget: float + adjPriceTarget: float + priceWhenPosted: float + newsPublisher: str + newsBaseURL: str = Field(alias="news_base_url") + analystCompany: str + + +class FMPPriceTargetFetcher( + Fetcher[ + PriceTargetQueryParams, + PriceTargetData, + FMPPriceTargetQueryParams, + FMPPriceTargetData, + ] +): + @staticmethod + def transform_query( + query: PriceTargetQueryParams, extra_params: Optional[Dict] = None + ) -> FMPPriceTargetQueryParams: + return FMPPriceTargetQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPPriceTargetQueryParams, api_key: str + ) -> List[FMPPriceTargetData]: + url = create_url(4, "price-target", api_key, query) + return get_data_many(url, FMPPriceTargetData) + + @staticmethod + def transform_data(data: List[FMPPriceTargetData]) -> List[PriceTargetData]: + return data_transformer(data, PriceTargetData) diff --git a/openbb_providers/builtin_providers/fmp/price_target_consensus.py b/openbb_providers/builtin_providers/fmp/price_target_consensus.py new file mode 100644 index 00000000000..7bd3cbe437d --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/price_target_consensus.py @@ -0,0 +1,79 @@ +"""FMP Price Target Consensus fetcher.""" + +# IMPORT STANDARD +from enum import Enum +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many +from openbb_provider.model.abstract.data import Data + +# IMPORT INTERNAL +from openbb_provider.model.data.price_target_consensus import ( + PriceTargetConsensusData, + PriceTargetConsensusQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class Interval(str, Enum): + oneminute = "1min" + fiveminute = "5min" + fifteenminute = "15min" + thirtyminute = "30min" + onehour = "1hour" + fourhour = "4hour" + day = "day" + + +class FMPPriceTargetConsensusQueryParams(PriceTargetConsensusQueryParams): + """FMP Price Target Consensus query. + + Source: https://site.financialmodelingprep.com/developer/docs/price-target-consensus-api/ + + Parameter + --------- + symbol : str + The symbol of the company. + """ + + __name__ = "FMPPriceTargetConsensusQueryParams" + + +class FMPPriceTargetConsensusData(Data): + __name__ = "FMPPriceTargetConsensusData" + + symbol: str + targetHigh: float + targetLow: float + targetConsensus: float + targetMedian: float + + +class FMPPriceTargetConsensusFetcher( + Fetcher[ + PriceTargetConsensusQueryParams, + PriceTargetConsensusData, + FMPPriceTargetConsensusQueryParams, + FMPPriceTargetConsensusData, + ] +): + @staticmethod + def transform_query( + query: PriceTargetConsensusQueryParams, extra_params: Optional[Dict] = None + ) -> FMPPriceTargetConsensusQueryParams: + return FMPPriceTargetConsensusQueryParams(symbol=query.symbol) + + @staticmethod + def extract_data( + query: FMPPriceTargetConsensusQueryParams, api_key: str + ) -> List[FMPPriceTargetConsensusData]: + url = create_url(4, "price-target-consensus", api_key, query) + return get_data_many(url, FMPPriceTargetConsensusData) + + @staticmethod + def transform_data( + data: List[FMPPriceTargetConsensusData], + ) -> List[PriceTargetConsensusData]: + return data_transformer(data, PriceTargetConsensusData) diff --git a/openbb_providers/builtin_providers/fmp/provider.py b/openbb_providers/builtin_providers/fmp/provider.py new file mode 100644 index 00000000000..378313b1168 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/provider.py @@ -0,0 +1,95 @@ +"""FMP Provider module.""" + +# IMPORT STANDARD + +# IMPORT THIRD-PARTY + +from builtin_providers.fmp.analyst_estimates import FMPAnalystEstimatesFetcher +from builtin_providers.fmp.balance_sheet import FMPBalanceSheetFetcher +from builtin_providers.fmp.cash_flow import FMPCashFlowStatementFetcher +from builtin_providers.fmp.company_profile import FMPCompanyProfileFetcher +from builtin_providers.fmp.crypto_eod import FMPCryptoEODFetcher +from builtin_providers.fmp.crypto_price import FMPCryptoPriceFetcher +from builtin_providers.fmp.earnings_calendar import FMPEarningsCalendarFetcher +from builtin_providers.fmp.earnings_call_transcript import ( + FMPEarningsCallTranscriptFetcher, +) +from builtin_providers.fmp.esg_risk_rating import FMPESGRiskRatingFetcher +from builtin_providers.fmp.esg_score import FMPESGScoreFetcher +from builtin_providers.fmp.esg_sector import FMPESGSectorFetcher +from builtin_providers.fmp.executive_compensation import FMPExecutiveCompensationFetcher +from builtin_providers.fmp.executives import FMPKeyExecutivesFetcher +from builtin_providers.fmp.forex_eod import FMPForexEODFetcher +from builtin_providers.fmp.forex_price import FMPForexPriceFetcher +from builtin_providers.fmp.global_news import FMPGlobalNewsFetcher +from builtin_providers.fmp.historical_dividends import FMPHistoricalDividendsFetcher +from builtin_providers.fmp.historical_stock_splits import ( + FMPHistoricalStockSplitsFetcher, +) +from builtin_providers.fmp.income_statement import FMPIncomeStatementFetcher +from builtin_providers.fmp.institutional_ownership import ( + FMPInstitutionalOwnershipFetcher, +) +from builtin_providers.fmp.key_metrics import FMPKeyMetricsFetcher +from builtin_providers.fmp.major_indices_eod import FMPMajorIndicesEODFetcher +from builtin_providers.fmp.major_indices_price import FMPMajorIndicesPriceFetcher +from builtin_providers.fmp.price_target import FMPPriceTargetFetcher +from builtin_providers.fmp.price_target_consensus import FMPPriceTargetConsensusFetcher +from builtin_providers.fmp.revenue_business_line import FMPRevenueBusinessLineFetcher +from builtin_providers.fmp.revenue_geographic import FMPRevenueGeographicFetcher +from builtin_providers.fmp.sec_filings import FMPSECFilingsFetcher +from builtin_providers.fmp.share_stats import FMPShareStatisticsFetcher +from builtin_providers.fmp.stock_eod import FMPStockEODFetcher +from builtin_providers.fmp.stock_insider_trading import FMPStockInsiderTradingFetcher +from builtin_providers.fmp.stock_news import FMPStockNewsFetcher +from builtin_providers.fmp.stock_ownership import FMPStockOwnershipFetcher +from builtin_providers.fmp.stock_price import FMPStockPriceFetcher +from builtin_providers.fmp.treasury_rates import FMPTreasuryRatesFetcher + +# IMPORT INTERNAL +from openbb_provider.provider.abstract.provider import Provider + +# mypy: disable-error-code="list-item" + +# ignoring because I dont know how to type the string properly +fmp_provider = Provider( + name="fmp", # type: ignore + description="Provider for FMP.", + fetcher_list=[ + FMPKeyExecutivesFetcher, + FMPStockEODFetcher, + FMPGlobalNewsFetcher, + FMPStockNewsFetcher, + FMPIncomeStatementFetcher, + FMPBalanceSheetFetcher, + FMPCashFlowStatementFetcher, + FMPShareStatisticsFetcher, + FMPMajorIndicesEODFetcher, + FMPRevenueGeographicFetcher, + FMPRevenueBusinessLineFetcher, + FMPInstitutionalOwnershipFetcher, + FMPCompanyProfileFetcher, + FMPStockInsiderTradingFetcher, + FMPStockOwnershipFetcher, + FMPESGScoreFetcher, + FMPESGSectorFetcher, + FMPESGRiskRatingFetcher, + FMPStockPriceFetcher, + FMPPriceTargetConsensusFetcher, + FMPPriceTargetFetcher, + FMPAnalystEstimatesFetcher, + FMPEarningsCalendarFetcher, + FMPEarningsCallTranscriptFetcher, + FMPHistoricalStockSplitsFetcher, + FMPHistoricalDividendsFetcher, + FMPKeyMetricsFetcher, + FMPSECFilingsFetcher, + FMPTreasuryRatesFetcher, + FMPExecutiveCompensationFetcher, + FMPCryptoPriceFetcher, + FMPCryptoEODFetcher, + FMPMajorIndicesPriceFetcher, + FMPForexEODFetcher, + FMPForexPriceFetcher, + ], +) diff --git a/openbb_providers/builtin_providers/fmp/revenue_business_line.py b/openbb_providers/builtin_providers/fmp/revenue_business_line.py new file mode 100644 index 00000000000..8ecfc3c3fd0 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/revenue_business_line.py @@ -0,0 +1,88 @@ +"""FMP Revenue Business Line Fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import validator + +from builtin_providers.fmp.helpers import create_url, get_data + +# IMPORT INTERNAL +from openbb_provider.model.data.revenue_business_line import ( + RevenueBusinessLineData, + RevenueBusinessLineQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + +# This part is only provided by FMP and not by the other providers for now. + + +class FMPRevenueBusinessLineQueryParams(RevenueBusinessLineQueryParams): + """FMP Revenue Business Line QueryParams. + + Source: https://site.financialmodelingprep.com/developer/docs/sales-revenue-by-segments-api/ + + Parameter + --------- + symbol : Optional[str] + The symbol of the company if no CIK is provided. + period : Literal["annual", "quarterly"] + The period of the income statement. Default is "annual". + structure : Literal["hierarchical", "flat"] + The structure of the revenue business line. Default is "flat". + """ + + __name__ = "FMPRevenueBusinessLineQueryParams" + + +class FMPRevenueBusinessLineData(RevenueBusinessLineData): + __name__ = "FMPRevenueBusinessLineData" + + @validator("date", pre=True) + def time_validate(cls, v): # pylint: disable=E0213 + return datetime.strptime(v, "%Y-%m-%d") + + +class FMPRevenueBusinessLineFetcher( + Fetcher[ # type: ignore + RevenueBusinessLineQueryParams, + RevenueBusinessLineData, + FMPRevenueBusinessLineQueryParams, + FMPRevenueBusinessLineData, + ] +): + @staticmethod + def transform_query( + query: RevenueBusinessLineQueryParams, extra_params: Optional[Dict] = None + ) -> FMPRevenueBusinessLineQueryParams: + return FMPRevenueBusinessLineQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPRevenueBusinessLineQueryParams, api_key: str + ) -> List[FMPRevenueBusinessLineData]: + # Type has to be ignored below because we are using something different than the literal type + query.period = "quarter" if query.period == "quarterly" else "annual" # type: ignore + url = create_url(4, "revenue-product-segmentation", api_key, query) + data = get_data(url) + if isinstance(data, dict): + raise ValueError("Expected list of dicts, got dict") + + data = [ + { + "date": list(d.keys())[0], + "data_and_service": list(d.values())[0], + } + for d in data + ] + + return [FMPRevenueBusinessLineData(**d) for d in data] + + @staticmethod + def transform_data( + data: List[FMPRevenueBusinessLineData], + ) -> List[RevenueBusinessLineData]: + return data_transformer(data, RevenueBusinessLineData) diff --git a/openbb_providers/builtin_providers/fmp/revenue_geographic.py b/openbb_providers/builtin_providers/fmp/revenue_geographic.py new file mode 100644 index 00000000000..cad1e0cff87 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/revenue_geographic.py @@ -0,0 +1,102 @@ +"""FMP Revenue Geographic Fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import validator + +from builtin_providers.fmp.helpers import create_url, get_data + +# IMPORT INTERNAL +from openbb_provider.model.data.revenue_geographic import ( + RevenueGeographicData, + RevenueGeographicQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + +# This part is only provided by FMP and not by the other providers for now. + + +class FMPRevenueGeographicQueryParams(RevenueGeographicQueryParams): + """FMP Revenue Geographic QueryParams. + + Source: https://financialmodelingprep.com/developer/docs/#Revenue-Geographic-Segmentation + + Parameter + --------- + symbol : Optional[str] + The symbol of the company if no CIK is provided. + period : Literal["annual", "quarterly"] + The period of the income statement. Default is "annual". + structure : Literal["hierarchical", "flat"] + The structure of the revenue geographic. Default is "flat". + """ + + __name__ = "FMPRevenueGeographicQueryParams" + + +class FMPRevenueGeographicData(RevenueGeographicData): + __name__ = "FMPRevenueGeographicData" + + @validator("date", pre=True) + def time_validate(cls, v): # pylint: disable=E0213 + return datetime.strptime(v, "%Y-%m-%d") + + +class FMPRevenueGeographicFetcher( + Fetcher[ # type: ignore + RevenueGeographicQueryParams, + RevenueGeographicData, + FMPRevenueGeographicQueryParams, + FMPRevenueGeographicData, + ] +): + @staticmethod + def transform_query( + query: RevenueGeographicQueryParams, extra_params: Optional[Dict] = None + ) -> FMPRevenueGeographicQueryParams: + return FMPRevenueGeographicQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPRevenueGeographicQueryParams, api_key: str + ) -> List[FMPRevenueGeographicData]: + query.period = "quarter" if query.period == "quarterly" else "annual" # type: ignore + url = create_url(4, "revenue-geographic-segmentation", api_key, query) + data = get_data(url) + if isinstance(data, dict): + raise ValueError("Expected list of dicts, got dict") + + data = [ + {**v, "date": k} for d in data for k, v in d.items() if isinstance(v, dict) + ] + + data = [ + { + "date": d.get("date"), + "americas": d.get("Americas Segment") + or d.get("North America Segment") + or d.get("Americas") + or d.get("North America"), + "europe": d.get("Europe Segment") or d.get("Europe"), + "greater_china": d.get("Greater China Segment") + or d.get("Greater China"), + "japan": d.get("Japan Segment") or d.get("Japan") or d.get("J P"), + "rest_of_asia_pacific": d.get("Rest of Asia Pacific Segment") + or d.get("Rest of Asia Pacific") + or d.get("Asia-Pacific"), + } + for d in data + ] + + return [FMPRevenueGeographicData(**d) for d in data] + + @staticmethod + def transform_data( + data: List[FMPRevenueGeographicData], + ) -> List[RevenueGeographicData]: + # Parse data to RevenueGeographicData + return data_transformer(data, RevenueGeographicData) diff --git a/openbb_providers/builtin_providers/fmp/sec_filings.py b/openbb_providers/builtin_providers/fmp/sec_filings.py new file mode 100644 index 00000000000..8da5ff894f6 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/sec_filings.py @@ -0,0 +1,83 @@ +"""SEC Filings fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from datetime import datetime +from typing import Dict, List, Optional + +from pydantic import validator + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data +from openbb_provider.model.data.sec_filings import SECFilingsData, SECFilingsQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPSECFilingsQueryParams(SECFilingsQueryParams): + """FMP SEC Filings QueryParams. + + Source: https://site.financialmodelingprep.com/developer/docs/sec-filings-api/ + + Parameter + --------- + symbol : str + The symbol of the company. + type : str + The type of the SEC filing form. (full list: https://www.sec.gov/forms) + page : int + The page of the results. + limit : int + The limit of the results. + """ + + __name__ = "FMPSECFilingsQueryParams" + + +class FMPSECFilingsData(Data): + __name__ = "FMPSECFilingsData" + + symbol: str + fillingDate: dateType + acceptedDate: dateType + cik: str + type: str + link: str + finalLink: str + + @validator("fillingDate", "acceptedDate", pre=True) + def convert_date(cls, v): # pylint: disable=no-self-argument + return datetime.strptime(v, "%Y-%m-%d %H:%M:%S").date() + + +class FMPSECFilingsFetcher( + Fetcher[ + SECFilingsQueryParams, + SECFilingsData, + FMPSECFilingsQueryParams, + FMPSECFilingsData, + ] +): + @staticmethod + def transform_query( + query: SECFilingsQueryParams, extra_params: Optional[Dict] = None + ) -> FMPSECFilingsQueryParams: + return FMPSECFilingsQueryParams( + symbol=query.symbol, page=query.page, limit=query.limit, type=query.type + ) + + @staticmethod + def extract_data( + query: FMPSECFilingsQueryParams, api_key: str + ) -> List[FMPSECFilingsData]: + url = create_url( + 3, f"sec_filings/{query.symbol}", api_key, query, exclude=["symbol"] + ) + return get_data_many(url, FMPSECFilingsData) + + @staticmethod + def transform_data(data: List[FMPSECFilingsData]) -> List[SECFilingsData]: + return data_transformer(data, SECFilingsData) diff --git a/openbb_providers/builtin_providers/fmp/share_stats.py b/openbb_providers/builtin_providers/fmp/share_stats.py new file mode 100644 index 00000000000..fbf4f733a73 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/share_stats.py @@ -0,0 +1,77 @@ +"""FMP Share Statistics Fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import validator + +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.base import BaseSymbol +from openbb_provider.model.data.share_statistics import ( + ShareStatisticsData, + ShareStatisticsQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPShareStatisticsQueryParams(QueryParams, BaseSymbol): + """FMP Income Statement QueryParams. + + Source: https://site.financialmodelingprep.com/developer/docs/shares-float-api/ + + Parameter + --------- + symbol : str + The symbol of the company. + """ + + __name__ = "FMPShareStatisticsQueryParams" + + +class FMPShareStatisticsData(Data): + __name__ = "FMPShareStatisticsData" + symbol: str + date: dateType + freeFloat: float + floatShares: float + outstandingShares: float + source: str + + @validator("date", pre=True) + def time_validate(cls, v): # pylint: disable=E0213 + return datetime.strptime(v, "%Y-%m-%d %H:%M:%S") + + +class FMPShareStatisticsFetcher( + Fetcher[ + ShareStatisticsQueryParams, + ShareStatisticsData, + FMPShareStatisticsQueryParams, + FMPShareStatisticsData, + ] +): + @staticmethod + def transform_query( + query: ShareStatisticsQueryParams, extra_params: Optional[Dict] = None + ) -> FMPShareStatisticsQueryParams: + return FMPShareStatisticsQueryParams(symbol=query.symbol) + + @staticmethod + def extract_data( + query: FMPShareStatisticsQueryParams, api_key: str + ) -> List[FMPShareStatisticsData]: + url = create_url(4, "shares_float", api_key, query) + return get_data_many(url, FMPShareStatisticsData) + + @staticmethod + def transform_data( + data: List[FMPShareStatisticsData], + ) -> List[ShareStatisticsData]: + return data_transformer(data, ShareStatisticsData) diff --git a/openbb_providers/builtin_providers/fmp/stock_eod.py b/openbb_providers/builtin_providers/fmp/stock_eod.py new file mode 100644 index 00000000000..943c831c229 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/stock_eod.py @@ -0,0 +1,99 @@ +"""FMP Stocks end of day fetcher.""" + +# IMPORT STANDARD +from datetime import date, datetime +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeInt, validator + +from builtin_providers.fmp.helpers import get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.stock_eod import StockEODData, StockEODQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + + +class FMPStockEODQueryParams(QueryParams): + """FMP Stock end of day query. + + Source: https://financialmodelingprep.com/developer/docs/#Stock-Historical-Price + + Parameter + --------- + symbol : str + The symbol of the company. + start_date : date + The start date of the stock data from which to retrieve the data. + end_date : date + The end date of the stock data up to which to retrieve the data. + timeseries : Optional[int] + The number of days to look back. + serietype : Optional[Literal["line"]] + The type of the series. Only "line" is supported. + """ + + __name__ = "FMPStockEODQueryParams" + symbol: str = Field(min_length=1) + serietype: Optional[Literal["line"]] + start_date: date + end_date: date + timeseries: Optional[NonNegativeInt] # Number of days to looks back + + +class FMPStockEODData(Data): + __name__ = "FMPStockEODData" + date: datetime + open: float + high: float + low: float + close: float + adjClose: float = Field(alias="adj_close") + volume: float + unadjustedVolume: float + change: float + changePercent: float + vwap: float + label: str + changeOverTime: float + + @validator("date", pre=True) + def time_validate(cls, v): # pylint: disable=E0213 + return datetime.strptime(v, "%Y-%m-%d") + + +class FMPStockEODFetcher( + Fetcher[ + StockEODQueryParams, + StockEODData, + FMPStockEODQueryParams, + FMPStockEODData, + ] +): + @staticmethod + def transform_query( + query: StockEODQueryParams, extra_params: Optional[Dict] = None + ) -> FMPStockEODQueryParams: + raw_end = query.end_date if query.end_date else datetime.now() + return FMPStockEODQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=raw_end, + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: FMPStockEODQueryParams, api_key: str + ) -> List[FMPStockEODData]: + base_url = "https://financialmodelingprep.com/api/v3/" + query_str = get_querystring(query.dict(), ["symbol"]) + query_str = query_str.replace("start_date", "from").replace("end_date", "to") + url = f"{base_url}historical-price-full/{query.symbol}?{query_str}&apikey={api_key}" + return get_data_many(url, FMPStockEODData, "historical") + + @staticmethod + def transform_data(data: List[FMPStockEODData]) -> List[StockEODData]: + return data_transformer(data, StockEODData) diff --git a/openbb_providers/builtin_providers/fmp/stock_insider_trading.py b/openbb_providers/builtin_providers/fmp/stock_insider_trading.py new file mode 100644 index 00000000000..57a4ab2c2a5 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/stock_insider_trading.py @@ -0,0 +1,74 @@ +"""FMP Stock Insider Trading fetcher.""" + +# IMPORT STANDARD +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.data.stock_insider_trading import ( + StockInsiderTradingData, + StockInsiderTradingQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPStockInsiderTradingQueryParams(StockInsiderTradingQueryParams): + """FMP Stock Insider Trading query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Stock-Insider-Trading + + Parameter + --------- + transactionType : List[TransactionTypes] + The type of the transaction. Possible values are: + A-Award, C-Conversion, D-Return, E-ExpireShort, F-InKind, G-Gift, H-ExpireLong + I-Discretionary, J-Other, L-Small, M-Exempt, O-OutOfTheMoneym P-Purchase + S-Sale, U-Tender, W-Will, X-InTheMoney, Z-Trust + symbol : Optional[str] + The symbol of the company. + reportingCik : Optional[str] + The CIK of the reporting owner. + companyCik: Optional[str] + The CIK of the company owner. + page: int + The page number to get + """ + + __name__ = "FMPStockInsiderTradingQueryParams" + + +class FMPStockInsiderTradingData(StockInsiderTradingData): + __name__ = "FMPStockInsiderTradingData" + + +class FMPStockInsiderTradingFetcher( + Fetcher[ + StockInsiderTradingQueryParams, + StockInsiderTradingData, + FMPStockInsiderTradingQueryParams, + FMPStockInsiderTradingData, + ] +): + @staticmethod + def transform_query( + query: StockInsiderTradingQueryParams, extra_params: Optional[Dict] = None + ) -> FMPStockInsiderTradingQueryParams: + return FMPStockInsiderTradingQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPStockInsiderTradingQueryParams, api_key: str + ) -> List[FMPStockInsiderTradingData]: + # This changes the actual type of a pydantic class, but its a quick and clean way to format properly + query.transactionType = ",".join(query.transactionType) # type: ignore + url = create_url(4, "insider-trading", api_key, query) + return get_data_many(url, FMPStockInsiderTradingData) + + @staticmethod + def transform_data( + data: List[FMPStockInsiderTradingData], + ) -> List[StockInsiderTradingData]: + return data_transformer(data, StockInsiderTradingData) diff --git a/openbb_providers/builtin_providers/fmp/stock_news.py b/openbb_providers/builtin_providers/fmp/stock_news.py new file mode 100644 index 00000000000..d6828ec3d24 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/stock_news.py @@ -0,0 +1,78 @@ +"""FMP Stock News fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeInt + +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.stock_news import StockNewsData, StockNewsQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPStockNewsQueryParams(QueryParams): + """FMP Stock News query. + + Source: https://site.financialmodelingprep.com/developer/docs/stock-news-api/ + + Parameter + --------- + symbols : str + The symbols of the companies. + page : int (default: 0) + The page of the data to retrieve. + limit : Optional[int] + The limit of the data to retrieve. + """ + + __name__ = "FMPStockNewsQueryParams" + tickers: str = Field(min_length=1, alias="symbols") + page: int = Field(default=0) + limit: Optional[NonNegativeInt] + + +class FMPStockNewsData(Data): + __name__ = "FMPStockNewsData" + symbol: str + publishedDate: datetime = Field(alias="date") + title: str + image: Optional[str] + text: str + url: str + site: str + + +class FMPStockNewsFetcher( + Fetcher[ + StockNewsQueryParams, + StockNewsData, + FMPStockNewsQueryParams, + FMPStockNewsData, + ] +): + @staticmethod + def transform_query( + query: StockNewsQueryParams, extra_params: Optional[Dict] = None + ) -> FMPStockNewsQueryParams: + return FMPStockNewsQueryParams( + symbols=query.symbols, + page=query.page, + **extra_params if extra_params else {} + ) + + @staticmethod + def extract_data( + query: FMPStockNewsQueryParams, api_key: str + ) -> List[FMPStockNewsData]: + url = create_url(3, "stock_news", api_key, query) + return get_data_many(url, FMPStockNewsData) + + @staticmethod + def transform_data(data: List[FMPStockNewsData]) -> List[StockNewsData]: + return data_transformer(data, StockNewsData) diff --git a/openbb_providers/builtin_providers/fmp/stock_ownership.py b/openbb_providers/builtin_providers/fmp/stock_ownership.py new file mode 100644 index 00000000000..f3d47c628a5 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/stock_ownership.py @@ -0,0 +1,68 @@ +"""FMP Stock Ownership fetcher.""" + +# IMPORT STANDARD +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from builtin_providers.fmp.helpers import create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.data.stock_ownership import ( + StockOwnershipData, + StockOwnershipQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class FMPStockOwnershipQueryParams(StockOwnershipQueryParams): + """FMP Stock Ownership query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Stock-Ownership-by-Holders + + Parameter + --------- + symbol : string + The symbol of the company. + page : int + The page number to get + date : date + The CIK of the company owner. + """ + + __name__ = "FMPStockOwnershipQueryParams" + + +class FMPStockOwnershipData(StockOwnershipData): + __name__ = "FMPStockOwnershipData" + + +class FMPStockOwnershipFetcher( + Fetcher[ + StockOwnershipQueryParams, + StockOwnershipData, + FMPStockOwnershipQueryParams, + FMPStockOwnershipData, + ] +): + @staticmethod + def transform_query( + query: StockOwnershipQueryParams, extra_params: Optional[Dict] = None + ) -> FMPStockOwnershipQueryParams: + return FMPStockOwnershipQueryParams.parse_obj(query) + + @staticmethod + def extract_data( + query: FMPStockOwnershipQueryParams, api_key: str + ) -> List[FMPStockOwnershipData]: + url = create_url( + 4, + "institutional-ownership/institutional-holders/symbol-ownership-percent", + api_key, + query, + ) + return get_data_many(url, FMPStockOwnershipData) + + @staticmethod + def transform_data(data: List[FMPStockOwnershipData]) -> List[StockOwnershipData]: + return data_transformer(data, StockOwnershipData) diff --git a/openbb_providers/builtin_providers/fmp/stock_price.py b/openbb_providers/builtin_providers/fmp/stock_price.py new file mode 100644 index 00000000000..4f9ad3d3142 --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/stock_price.py @@ -0,0 +1,77 @@ +"""FMP Stock Price fetcher.""" + +# IMPORT STANDARD +from enum import Enum +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.fmp.helpers import BaseStockPriceData, create_url, get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import QueryParams +from openbb_provider.model.data.stock_price import StockPriceData, StockPriceQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class Interval(str, Enum): + oneminute = "1min" + fiveminute = "5min" + fifteenminute = "15min" + thirtyminute = "30min" + onehour = "1hour" + fourhour = "4hour" + + +class FMPStockPriceQueryParams(QueryParams): + """FMP Stock Price query. + + Source: https://site.financialmodelingprep.com/developer/docs/#Stock-Historical-Price + + Parameter + --------- + symbol : str + The symbol of the company. + interval : Enum of ["1min", "5min", "15min", "30min", "1hour", "4hour"] (default: "1hour") + The interval of the data. + """ + + __name__ = "FMPStockPriceQueryParams" + symbol: str = Field(min_length=1) + interval: Literal["1min", "5min", "15min", "30min", "1hour", "4hour"] = "1hour" + + +class FMPStockPriceData(BaseStockPriceData): + __name__ = "FMPStockPriceData" + + +class FMPStockPriceFetcher( + Fetcher[ + StockPriceQueryParams, + StockPriceData, + FMPStockPriceQueryParams, + FMPStockPriceData, + ] +): + @staticmethod + def transform_query( + query: StockPriceQueryParams, extra_params: Optional[Dict] = None + ) -> FMPStockPriceQueryParams: + return FMPStockPriceQueryParams( + symbol=query.symbol, **extra_params if extra_params else {} + ) + + @staticmethod + def extract_data( + query: FMPStockPriceQueryParams, api_key: str + ) -> List[FMPStockPriceData]: + url = create_url( + 3, f"historical-chart/{query.interval}/{query.symbol}", api_key + ) + return get_data_many(url, FMPStockPriceData) + + @staticmethod + def transform_data(data: List[FMPStockPriceData]) -> List[StockPriceData]: + return data_transformer(data, StockPriceData) diff --git a/openbb_providers/builtin_providers/fmp/treasury_rates.py b/openbb_providers/builtin_providers/fmp/treasury_rates.py new file mode 100644 index 00000000000..21d7d92a9be --- /dev/null +++ b/openbb_providers/builtin_providers/fmp/treasury_rates.py @@ -0,0 +1,83 @@ +"""FMP Treasury Rates fetcher.""" + +# IMPORT STANDARD +from datetime import date as dateType +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.fmp.helpers import get_data_many + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data +from openbb_provider.model.data.treasury_rates import ( + TreasuryRatesData, + TreasuryRatesQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + + +class FMPTreasuryRatesQueryParams(TreasuryRatesQueryParams): + """FMP Stock News query. + + Source: https://site.financialmodelingprep.com/developer/docs/treasury-rates-api/ + + Parameter + --------- + start_date : str + The start date of the data. + end_date : str + The end date of the data. Default is today. + """ + + __name__ = "FMPTreasuryRatesQueryParams" + + +class FMPTreasuryRatesData(Data): + __name__ = "FMPTreasuryRatesData" + date: dateType + month1: float = Field(alias="month_1") + month2: float = Field(alias="month_2") + month3: float = Field(alias="month_3") + month6: float = Field(alias="month_6") + year1: float = Field(alias="year_1") + year2: float = Field(alias="year_2") + year3: float = Field(alias="year_3") + year5: float = Field(alias="year_5") + year7: float = Field(alias="year_7") + year10: float = Field(alias="year_10") + year20: float = Field(alias="year_20") + year30: float = Field(alias="year_30") + + +class FMPTreasuryRatesFetcher( + Fetcher[ + TreasuryRatesQueryParams, + TreasuryRatesData, + FMPTreasuryRatesQueryParams, + FMPTreasuryRatesData, + ] +): + @staticmethod + def transform_query( + query: TreasuryRatesQueryParams, extra_params: Optional[Dict] = None + ) -> FMPTreasuryRatesQueryParams: + return FMPTreasuryRatesQueryParams( + start_date=query.start_date, end_date=query.end_date + ) + + @staticmethod + def extract_data( + query: FMPTreasuryRatesQueryParams, api_key: str + ) -> List[FMPTreasuryRatesData]: + base_url = "https://financialmodelingprep.com/api/v4/" + query_str = get_querystring(query.dict(), []) + query_str = query_str.replace("start_date", "from").replace("end_date", "to") + url = f"{base_url}treasury?{query_str}&apikey={api_key}" + return get_data_many(url, FMPTreasuryRatesData) + + @staticmethod + def transform_data(data: List[FMPTreasuryRatesData]) -> List[TreasuryRatesData]: + return data_transformer(data, TreasuryRatesData) diff --git a/openbb_providers/builtin_providers/polygon/__init__.py b/openbb_providers/builtin_providers/polygon/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/openbb_providers/builtin_providers/polygon/balance_sheet.py b/openbb_providers/builtin_providers/polygon/balance_sheet.py new file mode 100644 index 00000000000..bcc365a331d --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/balance_sheet.py @@ -0,0 +1,100 @@ +# IMPORT STANDARD +from datetime import date as dateType +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import PolygonFundamentalQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data +from openbb_provider.model.data.balance_sheet import ( + BalanceSheetData, + BalanceSheetQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + + +class PolygonBalanceSheetQueryParams(PolygonFundamentalQueryParams): + """Source: https://polygon.io/docs/stocks#!/get_vx_reference_financials""" + + __name__ = "PolygonBalanceSheetQueryParams" + + +class PolygonBalanceSheetData(Data): + __name__ = "PolygonBalanceSheetData" + cik: Optional[str] + start_date: dateType = Field(alias="date") + assets: Optional[int] + current_assets: Optional[int] + current_liabilities: Optional[int] + equity: Optional[int] + equity_attributable_to_noncontrolling_interest: Optional[int] + equity_attributable_to_parent: Optional[int] + liabilities: Optional[int] + liabilities_and_equity: Optional[int] + noncurrent_assets: Optional[int] + noncurrent_liabilities: Optional[int] + + +class PolygonBalanceSheetFetcher( + Fetcher[ + BalanceSheetQueryParams, + BalanceSheetData, + PolygonBalanceSheetQueryParams, + PolygonBalanceSheetData, + ] +): + @staticmethod + def transform_query( + query: BalanceSheetQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonBalanceSheetQueryParams: + period = "annual" if query.period == "annually" else "quarterly" + return PolygonBalanceSheetQueryParams( + symbol=query.symbol, period=period, **extra_params if extra_params else {} # type: ignore + ) + + @staticmethod + def extract_data( + query: PolygonBalanceSheetQueryParams, api_key: str + ) -> List[PolygonBalanceSheetData]: + base_url = "https://api.polygon.io/vX/reference/financials" + query_string = get_querystring(query.dict(), []) + request_url = f"{base_url}?{query_string}&apiKey={api_key}" + data = get_data(request_url)["results"] + + if len(data) == 0: + raise RuntimeError("No balance sheet found") + + to_return = [] + for item in data: + new = {"start_date": item["start_date"]} + new["cik"] = item["cik"] + bs = item["financials"]["balance_sheet"] + new["assets"] = bs["assets"].get("value") + new["current_assets"] = bs["current_assets"].get("value") + new["current_liabilities"] = bs["current_liabilities"].get("value") + new["equity"] = bs["equity"].get("value") + new["equity_attributable_to_noncontrolling_interest"] = bs[ + "equity_attributable_to_noncontrolling_interest" + ].get("value") + new["equity_attributable_to_parent"] = bs[ + "equity_attributable_to_parent" + ].get("value") + new["equity"] = bs["equity"].get("value") + new["liabilities"] = bs["liabilities"].get("value") + new["liabilities_and_equity"] = bs["liabilities_and_equity"].get("value") + new["noncurrent_assets"] = bs["noncurrent_assets"].get("value") + new["noncurrent_liabilities"] = bs["noncurrent_liabilities"].get("value") + + to_return.append(PolygonBalanceSheetData(**new)) + return to_return + + @staticmethod + def transform_data( + data: List[PolygonBalanceSheetData], + ) -> List[BalanceSheetData]: + return data_transformer(data, BalanceSheetData) diff --git a/openbb_providers/builtin_providers/polygon/cash_flow.py b/openbb_providers/builtin_providers/polygon/cash_flow.py new file mode 100644 index 00000000000..6b06e165882 --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/cash_flow.py @@ -0,0 +1,118 @@ +# IMPORT STANDARD +from datetime import date as dateType +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import PolygonFundamentalQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data +from openbb_provider.model.data.cash_flows import ( + CashFlowStatementData, + CashFlowStatementQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + +# noqa: E501 + + +class PolygonCashFlowStatementQueryParams(PolygonFundamentalQueryParams): + """Source: https://polygon.io/docs/stocks#!/get_vx_reference_financials""" + + __name__ = "PolygonCashFlowStatementQueryParams" + + +class PolygonCashFlowStatementData(Data): + __name__ = "PolygonCashFlowStatementData" + start_date: dateType = Field(alias="date") + tickers: Optional[List[str]] = Field(alias="symbol") + cik: Optional[str] + filing_date: dateType + acceptance_datetime: Optional[datetime] + fiscal_period: Optional[str] = Field(alias="period") + # exchange_gain_losses: int + net_cash_flow: int + net_cash_flow_from_financing_activities: int + net_cash_flow_from_investing_activities: int + net_cash_flow_from_operating_activities: int + net_cash_flow_continuing: int + net_cash_flow_from_financing_activities_continuing: int + net_cash_flow_from_investing_activities_continuing: int + net_cash_flow_from_operating_activities_continuing: int + + +class PolygonCashFlowStatementFetcher( + Fetcher[ + CashFlowStatementQueryParams, + CashFlowStatementData, + PolygonCashFlowStatementQueryParams, + PolygonCashFlowStatementData, + ] +): + @staticmethod + def transform_query( + query: CashFlowStatementQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonCashFlowStatementQueryParams: + period = "annual" if query.period == "annually" else "quarterly" + return PolygonCashFlowStatementQueryParams( + symbol=query.symbol, period=period, **extra_params if extra_params else {} # type: ignore + ) + + @staticmethod + def extract_data( + query: PolygonCashFlowStatementQueryParams, api_key: str + ) -> List[PolygonCashFlowStatementData]: + base_url = "https://api.polygon.io/vX/reference/financials" + query_string = get_querystring(query.dict(), []) + request_url = f"{base_url}?{query_string}&apiKey={api_key}" + data = get_data(request_url)["results"] + + if len(data) == 0: + raise RuntimeError("No Cash Flow Statement found") + + to_return = [] + for item in data: + new = {"acceptance_datetime": item.get("acceptance_datetime")} + new["start_date"] = item["start_date"] + new["filing_date"] = item.get("filing_date") + new["cik"] = item["cik"] + new["fiscal_period"] = item["fiscal_period"] + new["tickers"] = item["tickers"] + cf = item["financials"]["cash_flow_statement"] + new["net_cash_flow_from_financing_activities"] = cf[ + "net_cash_flow_from_financing_activities" + ].get("value") + new["net_cash_flow_from_investing_activities"] = cf[ + "net_cash_flow_from_investing_activities" + ].get("value") + new["net_cash_flow_from_operating_activities"] = cf[ + "net_cash_flow_from_operating_activities" + ].get("value") + new["net_cash_flow_continuing"] = cf["net_cash_flow_continuing"].get( + "value" + ) + new["net_cash_flow_from_financing_activities_continuing"] = cf[ + "net_cash_flow_from_financing_activities_continuing" + ].get("value") + new["net_cash_flow_from_investing_activities_continuing"] = cf[ + "net_cash_flow_from_investing_activities_continuing" + ].get("value") + new["net_cash_flow_from_operating_activities_continuing"] = cf[ + "net_cash_flow_from_operating_activities_continuing" + ].get("value") + new["net_cash_flow"] = cf["net_cash_flow"].get("value") + + to_return.append(PolygonCashFlowStatementData(**new)) + return to_return + + @staticmethod + def transform_data( + data: List[PolygonCashFlowStatementData], + ) -> List[CashFlowStatementData]: + processors = {"tickers": lambda x: "" if not x else ",".join(x)} + return data_transformer(data, CashFlowStatementData, processors) diff --git a/openbb_providers/builtin_providers/polygon/crypto_eod.py b/openbb_providers/builtin_providers/polygon/crypto_eod.py new file mode 100644 index 00000000000..d677f317472 --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/crypto_eod.py @@ -0,0 +1,97 @@ +"""Polygon crypto end of day fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeFloat, PositiveFloat, PositiveInt + +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import BaseStockData, BaseStockQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.data.crypto_eod import CryptoEODData, CryptoEODQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class PolygonCryptoEODQueryParams(BaseStockQueryParams): + """Polygon crypto end of day query. + + Source: https://polygon.io/docs/crypto/get_v2_aggs_ticker__cryptoticker__range__multiplier___timespan___from___to + + Parameters + ---------- + symbol : str + The symbol symbol of the stocks to fetch. + start_date : Union[date, datetime] + The start date of the query. + end_date : Union[date, datetime] + The end date of the query. + timespan : Timespan, optional + The timespan of the query, by default Timespan.day + sort : Literal["asc", "desc"], optional + The sort order of the query, by default "desc" + limit : PositiveInt, optional + The limit of the query, by default 49999 + adjusted : bool, optional + Whether the query is adjusted, by default True + multiplier : PositiveInt, optional + The multiplier of the query, by default 1 + """ + + __name__ = "PolygonCryptoEODQueryParams" + + +class PolygonCryptoEODData(BaseStockData): + __name__ = "PolygonEODCryptoData" + v: NonNegativeFloat = Field(alias="volume") + n: PositiveInt + vw: PositiveFloat + + +class PolygonCryptoEODFetcher( + Fetcher[ + CryptoEODQueryParams, + CryptoEODData, + PolygonCryptoEODQueryParams, + PolygonCryptoEODData, + ] +): + @staticmethod + def transform_query( + query: CryptoEODQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonCryptoEODQueryParams: + return PolygonCryptoEODQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=query.end_date if query.end_date else datetime.now(), + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: PolygonCryptoEODQueryParams, api_key: str + ) -> List[PolygonCryptoEODData]: + request_url = ( + f"https://api.polygon.io/v2/aggs/ticker/" + f"X:{query.stocksTicker.upper()}/range/1/{query.timespan}/" + f"{query.start_date}/{query.end_date}?adjusted={query.adjusted}" + f"&sort={query.sort}&limit={query.limit}&multiplier={query.multiplier}" + f"&apiKey={api_key}" + ) + + data = get_data(request_url) + if isinstance(data, list): + raise ValueError("Expected a dict, got a list") + + if "results" not in data.keys() or len(data["results"]) == 0: + raise RuntimeError("No results found. Please change your query parameters.") + + data = data["results"] + return [PolygonCryptoEODData(**d) for d in data] + + @staticmethod + def transform_data(data: List[PolygonCryptoEODData]) -> List[CryptoEODData]: + return data_transformer(data, CryptoEODData) diff --git a/openbb_providers/builtin_providers/polygon/crypto_price.py b/openbb_providers/builtin_providers/polygon/crypto_price.py new file mode 100644 index 00000000000..98bbc2c714f --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/crypto_price.py @@ -0,0 +1,100 @@ +"""Polygon Crypto Price fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeFloat, PositiveFloat, PositiveInt + +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import BaseStockData, BaseStockQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.data.crypto_price import ( + CryptoPriceData, + CryptoPriceQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class PolygonCryptoPriceQueryParams(BaseStockQueryParams): + """Polygon crypto price query. + + Source: https://polygon.io/docs/crypto/get_v2_aggs_ticker__cryptoticker__range__multiplier___timespan___from___to + + Parameters + ---------- + stocksTicker : str + The ticker symbol of the stocks to fetch. + start_date : Union[date, datetime] + The start date of the query. + end_date : Union[date, datetime] + The end date of the query. + timespan : Timespan, optional + The timespan of the query, by default Timespan.day + sort : Literal["asc", "desc"], optional + The sort order of the query, by default "desc" + limit : PositiveInt, optional + The limit of the query, by default 49999 + adjusted : bool, optional + Whether the query is adjusted, by default True + multiplier : PositiveInt, optional + The multiplier of the query, by default 1 + """ + + __name__ = "PolygonCryptoPriceQueryParams" + + +class PolygonCryptoPriceData(BaseStockData): + __name__ = "PolygonPriceCryptoData" + v: NonNegativeFloat = Field(alias="volume") + n: PositiveInt + vw: PositiveFloat + + +class PolygonCryptoPriceFetcher( + Fetcher[ + CryptoPriceQueryParams, + CryptoPriceData, + PolygonCryptoPriceQueryParams, + PolygonCryptoPriceData, + ] +): + @staticmethod + def transform_query( + query: CryptoPriceQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonCryptoPriceQueryParams: + return PolygonCryptoPriceQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=query.end_date if query.end_date else datetime.now(), + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: PolygonCryptoPriceQueryParams, api_key: str + ) -> List[PolygonCryptoPriceData]: + request_url = ( + f"https://api.polygon.io/v2/aggs/ticker/" + f"X:{query.stocksTicker.upper()}/range/1/{query.timespan}/" + f"{query.start_date}/{query.end_date}?adjusted={query.adjusted}" + f"&sort={query.sort}&limit={query.limit}&multiplier={query.multiplier}" + f"&apiKey={api_key}" + ) + + data = get_data(request_url) + if isinstance(data, list): + raise ValueError("Expected a dict, got a list") + + if "results" not in data.keys() or len(data["results"]) == 0: + raise RuntimeError("No results found. Please change your query parameters.") + + data = data["results"] + return [PolygonCryptoPriceData(**d) for d in data] + + @staticmethod + def transform_data(data: List[PolygonCryptoPriceData]) -> List[CryptoPriceData]: + return data_transformer(data, CryptoPriceData) diff --git a/openbb_providers/builtin_providers/polygon/forex_eod.py b/openbb_providers/builtin_providers/polygon/forex_eod.py new file mode 100644 index 00000000000..ead6b9cd684 --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/forex_eod.py @@ -0,0 +1,97 @@ +"""Polygon forex end of day fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeFloat, PositiveFloat, PositiveInt + +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import BaseStockData, BaseStockQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.data.forex_eod import ForexEODData, ForexEODQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class PolygonForexEODQueryParams(BaseStockQueryParams): + """Polygon forex end of day query. + + Source: https://polygon.io/docs/forex/get_v2_aggs_ticker__forexticker__range__multiplier___timespan___from___to + + Parameters + ---------- + stocksTicker : str + The ticker symbol of the stocks to fetch. + start_date : Union[date, datetime] + The start date of the query. + end_date : Union[date, datetime] + The end date of the query. + timespan : Timespan, optional + The timespan of the query, by default Timespan.day + sort : Literal["asc", "desc"], optional + The sort order of the query, by default "desc" + limit : PositiveInt, optional + The limit of the query, by default 49999 + adjusted : bool, optional + Whether the query is adjusted, by default True + multiplier : PositiveInt, optional + The multiplier of the query, by default 1 + """ + + __name__ = "PolygonForexEODQueryParams" + + +class PolygonForexEODData(BaseStockData): + __name__ = "PolygonEODForexData" + v: NonNegativeFloat = Field(alias="volume") + n: PositiveInt + vw: Optional[PositiveFloat] + + +class PolygonForexEODFetcher( + Fetcher[ + ForexEODQueryParams, + ForexEODData, + PolygonForexEODQueryParams, + PolygonForexEODData, + ] +): + @staticmethod + def transform_query( + query: ForexEODQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonForexEODQueryParams: + return PolygonForexEODQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=query.end_date if query.end_date else datetime.now(), + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: PolygonForexEODQueryParams, api_key: str + ) -> List[PolygonForexEODData]: + request_url = ( + f"https://api.polygon.io/v2/aggs/ticker/" + f"C:{query.stocksTicker.upper()}/range/1/{query.timespan}/" + f"{query.start_date}/{query.end_date}?adjusted={query.adjusted}" + f"&sort={query.sort}&limit={query.limit}&multiplier={query.multiplier}" + f"&apiKey={api_key}" + ) + + data = get_data(request_url) + if isinstance(data, list): + raise ValueError("Expected a dict, got a list") + + if "results" not in data.keys() or len(data["results"]) == 0: + raise RuntimeError("No results found. Please change your query parameters.") + + data = data["results"] + return [PolygonForexEODData(**d) for d in data] + + @staticmethod + def transform_data(data: List[PolygonForexEODData]) -> List[ForexEODData]: + return data_transformer(data, ForexEODData) diff --git a/openbb_providers/builtin_providers/polygon/forex_price.py b/openbb_providers/builtin_providers/polygon/forex_price.py new file mode 100644 index 00000000000..5088c2f2d93 --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/forex_price.py @@ -0,0 +1,97 @@ +"""Polygon Forex Price fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeFloat, PositiveFloat, PositiveInt + +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import BaseStockData, BaseStockQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.data.forex_price import ForexPriceData, ForexPriceQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class PolygonForexPriceQueryParams(BaseStockQueryParams): + """Polygon forex price query. + + Source: https://polygon.io/docs/forex/get_v2_aggs_ticker__forexticker__range__multiplier___timespan___from___to + + Parameters + ---------- + stocksTicker : str + The ticker symbol of the stocks to fetch. + start_date : Union[date, datetime] + The start date of the query. + end_date : Union[date, datetime] + The end date of the query. + timespan : Timespan, optional + The timespan of the query, by default Timespan.day + sort : Literal["asc", "desc"], optional + The sort order of the query, by default "desc" + limit : PositiveInt, optional + The limit of the query, by default 49999 + adjusted : bool, optional + Whether the query is adjusted, by default True + multiplier : PositiveInt, optional + The multiplier of the query, by default 1 + """ + + __name__ = "PolygonForexPriceQueryParams" + + +class PolygonForexPriceData(BaseStockData): + __name__ = "PolygonPriceForexData" + v: NonNegativeFloat = Field(alias="volume") + n: PositiveInt + vw: Optional[PositiveFloat] + + +class PolygonForexPriceFetcher( + Fetcher[ + ForexPriceQueryParams, + ForexPriceData, + PolygonForexPriceQueryParams, + PolygonForexPriceData, + ] +): + @staticmethod + def transform_query( + query: ForexPriceQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonForexPriceQueryParams: + return PolygonForexPriceQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=query.end_date if query.end_date else datetime.now(), + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: PolygonForexPriceQueryParams, api_key: str + ) -> List[PolygonForexPriceData]: + request_url = ( + f"https://api.polygon.io/v2/aggs/ticker/" + f"C:{query.stocksTicker.upper()}/range/1/{query.timespan}/" + f"{query.start_date}/{query.end_date}?adjusted={query.adjusted}" + f"&sort={query.sort}&limit={query.limit}&multiplier={query.multiplier}" + f"&apiKey={api_key}" + ) + + data = get_data(request_url) + if isinstance(data, list): + raise ValueError("Expected a dict, got a list") + + if "results" not in data.keys() or len(data["results"]) == 0: + raise RuntimeError("No results found. Please change your query parameters.") + + data = data["results"] + return [PolygonForexPriceData(**d) for d in data] + + @staticmethod + def transform_data(data: List[PolygonForexPriceData]) -> List[ForexPriceData]: + return data_transformer(data, ForexPriceData) diff --git a/openbb_providers/builtin_providers/polygon/helpers.py b/openbb_providers/builtin_providers/polygon/helpers.py new file mode 100644 index 00000000000..c05bc3faf4e --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/helpers.py @@ -0,0 +1,19 @@ +# IMPORT STANDARD + + +# IMPORT THIRD PARTY +import requests + +# IMPORT INTERNAL + + +def get_data(url: str) -> dict: + r = requests.get(url, timeout=10) + if r.status_code != 200: + data = r.json() + message = data.get("message") + error = data.get("error") + value = message or error + raise RuntimeError(f"Error in Polygon request: {value}") + + return r.json() diff --git a/openbb_providers/builtin_providers/polygon/income_statement.py b/openbb_providers/builtin_providers/polygon/income_statement.py new file mode 100644 index 00000000000..afadc961aab --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/income_statement.py @@ -0,0 +1,155 @@ +# IMPORT STANDARD +from datetime import date as dateType +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field + +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import PolygonFundamentalQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data +from openbb_provider.model.data.income_statement import ( + IncomeStatementData, + IncomeStatementQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + + +class PolygonIncomeStatementQueryParams(PolygonFundamentalQueryParams): + """Source: https://polygon.io/docs/stocks#!/get_vx_reference_financials""" + + __name__ = "PolygonIncomeStatementQueryParams" + + +class PolygonIncomeStatementData(Data): + __name__ = "PolygonIncomeStatementData" + start_date: dateType = Field(alias="date") + tickers: Optional[List[str]] = Field(alias="symbol") + cik: Optional[str] + filing_date: dateType + acceptance_datetime: Optional[datetime] = Field(alias="accepted_date") + fiscal_period: Optional[str] = Field(alias="period") + revenues: Optional[float] = Field(alias="revenue") + cost_of_revenue: Optional[float] + gross_profit: Optional[float] + operating_expenses: Optional[float] + costs_and_expenses: Optional[float] + income_loss_from_continuing_operations_before_tax: Optional[float] = Field( + alias="income_before_tax" + ) + income_loss_from_continuing_operations_after_tax: Optional[float] + income_tax_expense_benefit: Optional[float] = Field(alias="income_tax_expense") + net_income_loss: Optional[float] = Field(alias="net_income") + basic_earnings_per_share: Optional[float] = Field(alias="eps") + diluted_earnings_per_share: Optional[float] = Field(alias="eps_diluted") + benefits_costs_expenses: Optional[float] + interest_expense_operating: Optional[float] = Field(alias="interest_expense") + net_income_loss_attributable_to_noncontrolling_interest: Optional[int] + net_income_loss_attributable_to_parent: Optional[float] + net_income_loss_available_to_common_stockholders_basic: Optional[float] + operating_income_loss: Optional[float] + participating_securities_distributed_and_undistributed_earnings_loss_basic: Optional[ + float + ] + preferred_stock_dividends_and_other_adjustments: Optional[float] + + +class PolygonIncomeStatementFetcher( + Fetcher[ + IncomeStatementQueryParams, + IncomeStatementData, + PolygonIncomeStatementQueryParams, + PolygonIncomeStatementData, + ] +): + @staticmethod + def transform_query( + query: IncomeStatementQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonIncomeStatementQueryParams: + period = "annual" if query.period == "annually" else "quarterly" + return PolygonIncomeStatementQueryParams( + symbol=query.symbol, period=period, **extra_params if extra_params else {} # type: ignore + ) + + @staticmethod + def extract_data( + query: PolygonIncomeStatementQueryParams, api_key: str + ) -> List[PolygonIncomeStatementData]: + base_url = "https://api.polygon.io/vX/reference/financials" + query_string = get_querystring(query.dict(), []) + request_url = f"{base_url}?{query_string}&apiKey={api_key}" + data = get_data(request_url)["results"] + + if len(data) == 0: + raise RuntimeError("No Income Statement found") + + to_return = [] + for item in data: + new = {"acceptance_datetime": item.get("acceptance_datetime")} + new["start_date"] = item["start_date"] + new["filing_date"] = item.get("filing_date") + new["cik"] = item["cik"] + new["fiscal_period"] = item["fiscal_period"] + new["tickers"] = item["tickers"] + to_return.append(PolygonIncomeStatementData(**new)) + incs = item["financials"]["income_statement"] + new["revenues"] = incs["revenues"].get("value") + new["cost_of_revenue"] = incs["cost_of_revenue"].get("value") + new["gross_profit"] = incs["gross_profit"].get("value") + new["operating_expenses"] = incs["operating_expenses"].get("value") + new["costs_and_expenses"] = incs["costs_and_expenses"].get("value") + new["income_loss_from_continuing_operations_before_tax"] = incs[ + "income_loss_from_continuing_operations_before_tax" + ].get("value") + new["income_tax_expense_benefit"] = incs["income_tax_expense_benefit"].get( + "value" + ) + new["net_income_loss"] = incs["net_income_loss"].get("value") + new["basic_earnings_per_share"] = incs.get( + "basic_earnings_per_share", {} + ).get("value") + new["diluted_earnings_per_share"] = incs.get( + "diluted_earnings_per_share", {} + ).get("value") + new["net_income_loss_attributable_to_noncontrolling_interest"] = incs[ + "net_income_loss_attributable_to_noncontrolling_interest" + ].get("value") + new["net_income_loss_attributable_to_parent"] = incs[ + "net_income_loss_attributable_to_parent" + ].get("value") + new["net_income_loss_available_to_common_stockholders_basic"] = incs[ + "net_income_loss_available_to_common_stockholders_basic" + ].get("value") + new["operating_income_loss"] = incs["operating_income_loss"].get("value") + new[ + "participating_securities_distributed_and_undistributed_earnings_loss_basic" + ] = incs[ + "participating_securities_distributed_and_undistributed_earnings_loss_basic" + ].get( + "value" + ) + new["preferred_stock_dividends_and_other_adjustments"] = incs[ + "preferred_stock_dividends_and_other_adjustments" + ].get("value") + new["income_loss_from_continuing_operations_after_tax"] = incs[ + "income_loss_from_continuing_operations_after_tax" + ].get("value") + new["benefits_costs_expenses"] = incs["benefits_costs_expenses"].get( + "value" + ) + new["interest_expense_operating"] = incs["interest_expense_operating"].get( + "value" + ) + to_return.append(PolygonIncomeStatementData(**new)) + return to_return + + @staticmethod + def transform_data( + data: List[PolygonIncomeStatementData], + ) -> List[IncomeStatementData]: + processors = {"tickers": lambda x: "" if not x else ",".join(x)} + return data_transformer(data, IncomeStatementData, processors) diff --git a/openbb_providers/builtin_providers/polygon/major_indices_eod.py b/openbb_providers/builtin_providers/polygon/major_indices_eod.py new file mode 100644 index 00000000000..231a065edfa --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/major_indices_eod.py @@ -0,0 +1,97 @@ +"""Polygon major indices end of day fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import BaseStockData, BaseStockQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.data.major_indices_eod import ( + MajorIndicesEODData, + MajorIndicesEODQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class PolygonMajorIndicesEODQueryParams(BaseStockQueryParams): + """Polygon major indices end of day query. + + Source: https://polygon.io/docs/indices/getting-started + + Parameters + ---------- + symbol : str + The symbol of the stocks to fetch. + start_date : Union[date, datetime] + The start date of the query. + end_date : Union[date, datetime] + The end date of the query. + timespan : Timespan, optional + The timespan of the query, by default Timespan.day + sort : Literal["asc", "desc"], optional + The sort order of the query, by default "desc" + limit : PositiveInt, optional + The limit of the query, by default 49999 + adjusted : bool, optional + Whether the query is adjusted, by default True + multiplier : PositiveInt, optional + The multiplier of the query, by default 1 + """ + + __name__ = "PolygonMajorIndicesEODQueryParams" + + +class PolygonMajorIndicesEODData(BaseStockData): + __name__ = "PolygonEODMajorIndicesData" + + +class PolygonMajorIndicesEODFetcher( + Fetcher[ + MajorIndicesEODQueryParams, + MajorIndicesEODData, + PolygonMajorIndicesEODQueryParams, + PolygonMajorIndicesEODData, + ] +): + @staticmethod + def transform_query( + query: MajorIndicesEODQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonMajorIndicesEODQueryParams: + return PolygonMajorIndicesEODQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=query.end_date if query.end_date else datetime.now(), + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: PolygonMajorIndicesEODQueryParams, api_key: str + ) -> List[PolygonMajorIndicesEODData]: + request_url = ( + f"https://api.polygon.io/v2/aggs/ticker/" + f"I:{query.stocksTicker.upper()}/range/1/{query.timespan}/" + f"{query.start_date}/{query.end_date}?adjusted={query.adjusted}" + f"&sort={query.sort}&limit={query.limit}&multiplier={query.multiplier}" + f"&apiKey={api_key}" + ) + + data = get_data(request_url) + if isinstance(data, list): + raise ValueError("Expected a dict, got a list") + + if "results" not in data.keys() or len(data["results"]) == 0: + raise RuntimeError("No results found. Please change your query parameters.") + + data = data["results"] + return [PolygonMajorIndicesEODData(**d) for d in data] + + @staticmethod + def transform_data( + data: List[PolygonMajorIndicesEODData], + ) -> List[MajorIndicesEODData]: + return data_transformer(data, MajorIndicesEODData) diff --git a/openbb_providers/builtin_providers/polygon/major_indices_price.py b/openbb_providers/builtin_providers/polygon/major_indices_price.py new file mode 100644 index 00000000000..39f56ae6c7f --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/major_indices_price.py @@ -0,0 +1,97 @@ +"""Polygon major indices end of day fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import BaseStockData, BaseStockQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.data.major_indices_price import ( + MajorIndicesPriceData, + MajorIndicesPriceQueryParams, +) +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class PolygonMajorIndicesPriceQueryParams(BaseStockQueryParams): + """Polygon major indices price query. + + Source: https://polygon.io/docs/indices/getting-started + + Parameters + ---------- + symbol : str + The symbol of the stocks to fetch. + start_date : Union[date, datetime] + The start date of the query. + end_date : Union[date, datetime] + The end date of the query. + timespan : Timespan, optional + The timespan of the query, by default Timespan.day + sort : Literal["asc", "desc"], optional + The sort order of the query, by default "desc" + limit : PositiveInt, optional + The limit of the query, by default 49999 + adjusted : bool, optional + Whether the query is adjusted, by default True + multiplier : PositiveInt, optional + The multiplier of the query, by default 1 + """ + + __name__ = "PolygonMajorIndicesPriceQueryParams" + + +class PolygonMajorIndicesPriceData(BaseStockData): + __name__ = "PolygonPriceMajorIndicesData" + + +class PolygonMajorIndicesPriceFetcher( + Fetcher[ + MajorIndicesPriceQueryParams, + MajorIndicesPriceData, + PolygonMajorIndicesPriceQueryParams, + PolygonMajorIndicesPriceData, + ] +): + @staticmethod + def transform_query( + query: MajorIndicesPriceQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonMajorIndicesPriceQueryParams: + return PolygonMajorIndicesPriceQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=query.end_date if query.end_date else datetime.now(), + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: PolygonMajorIndicesPriceQueryParams, api_key: str + ) -> List[PolygonMajorIndicesPriceData]: + request_url = ( + f"https://api.polygon.io/v2/aggs/ticker/" + f"I:{query.stocksTicker.upper()}/range/1/{query.timespan}/" + f"{query.start_date}/{query.end_date}?adjusted={query.adjusted}" + f"&sort={query.sort}&limit={query.limit}&multiplier={query.multiplier}" + f"&apiKey={api_key}" + ) + + data = get_data(request_url) + if isinstance(data, list): + raise ValueError("Expected a dict, got a list") + + if "results" not in data.keys() or len(data["results"]) == 0: + raise RuntimeError("No results found. Please change your query parameters.") + + data = data["results"] + return [PolygonMajorIndicesPriceData(**d) for d in data] + + @staticmethod + def transform_data( + data: List[PolygonMajorIndicesPriceData], + ) -> List[MajorIndicesPriceData]: + return data_transformer(data, MajorIndicesPriceData) diff --git a/openbb_providers/builtin_providers/polygon/provider.py b/openbb_providers/builtin_providers/polygon/provider.py new file mode 100644 index 00000000000..8904c9fbafa --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/provider.py @@ -0,0 +1,44 @@ +"""Polygon provider module.""" + +# IMPORT STANDARD + +from builtin_providers.polygon.balance_sheet import PolygonBalanceSheetFetcher +from builtin_providers.polygon.cash_flow import PolygonCashFlowStatementFetcher +from builtin_providers.polygon.crypto_eod import PolygonCryptoEODFetcher +from builtin_providers.polygon.crypto_price import PolygonCryptoPriceFetcher +from builtin_providers.polygon.forex_eod import PolygonForexEODFetcher +from builtin_providers.polygon.forex_price import PolygonForexPriceFetcher +from builtin_providers.polygon.income_statement import PolygonIncomeStatementFetcher +from builtin_providers.polygon.major_indices_eod import PolygonMajorIndicesEODFetcher +from builtin_providers.polygon.major_indices_price import ( + PolygonMajorIndicesPriceFetcher, +) +from builtin_providers.polygon.stock_eod import PolygonStockEODFetcher +from builtin_providers.polygon.stock_news import PolygonStockNewsFetcher +from builtin_providers.polygon.stock_price import PolygonStockPriceFetcher + +# IMPORT INTERNAL +from openbb_provider.provider.abstract.provider import Provider + +# IMPORT THIRD-PARTY + +# mypy: disable-error-code="list-item" +# ignoring because I dont know how to type the string properly +polygon_provider = Provider( + name="polygon", # type: ignore + description="Provider for Polygon.", + fetcher_list=[ + PolygonStockEODFetcher, + PolygonStockNewsFetcher, + PolygonBalanceSheetFetcher, + PolygonIncomeStatementFetcher, + PolygonCashFlowStatementFetcher, + PolygonStockPriceFetcher, + PolygonCryptoPriceFetcher, + PolygonCryptoEODFetcher, + PolygonMajorIndicesEODFetcher, + PolygonMajorIndicesPriceFetcher, + PolygonForexEODFetcher, + PolygonForexPriceFetcher, + ], +) diff --git a/openbb_providers/builtin_providers/polygon/stock_eod.py b/openbb_providers/builtin_providers/polygon/stock_eod.py new file mode 100644 index 00000000000..78eb8b1ca14 --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/stock_eod.py @@ -0,0 +1,97 @@ +"""Polygon stocks end of day fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeFloat, PositiveFloat, PositiveInt + +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import BaseStockData, BaseStockQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.data.stock_eod import StockEODData, StockEODQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class PolygonStockEODQueryParams(BaseStockQueryParams): + """Polygon stocks end of day query. + + Source: https://polygon.io/docs/stocks/getting-started + + Parameters + ---------- + symbol : str + The symbol of the stocks to fetch. + start_date : Union[date, datetime] + The start date of the query. + end_date : Union[date, datetime] + The end date of the query. + timespan : Timespan, optional + The timespan of the query, by default Timespan.day + sort : Literal["asc", "desc"], optional + The sort order of the query, by default "desc" + limit : PositiveInt, optional + The limit of the query, by default 49999 + adjusted : bool, optional + Whether the query is adjusted, by default True + multiplier : PositiveInt, optional + The multiplier of the query, by default 1 + """ + + __name__ = "PolygonStockEODQueryParams" + + +class PolygonStockEODData(BaseStockData): + __name__ = "PolygonEODStockData" + v: NonNegativeFloat = Field(alias="volume") + n: PositiveInt + vw: PositiveFloat + + +class PolygonStockEODFetcher( + Fetcher[ + StockEODQueryParams, + StockEODData, + PolygonStockEODQueryParams, + PolygonStockEODData, + ] +): + @staticmethod + def transform_query( + query: StockEODQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonStockEODQueryParams: + return PolygonStockEODQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=query.end_date if query.end_date else datetime.now(), + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: PolygonStockEODQueryParams, api_key: str + ) -> List[PolygonStockEODData]: + request_url = ( + f"https://api.polygon.io/v2/aggs/ticker/" + f"{query.stocksTicker.upper()}/range/1/{query.timespan}/" + f"{query.start_date}/{query.end_date}?adjusted={query.adjusted}" + f"&sort={query.sort}&limit={query.limit}&multiplier={query.multiplier}" + f"&apiKey={api_key}" + ) + + data = get_data(request_url) + if isinstance(data, list): + raise ValueError("Expected a dict, got a list") + + if "results" not in data.keys() or len(data["results"]) == 0: + raise RuntimeError("No results found. Please change your query parameters.") + + data = data["results"] + return [PolygonStockEODData(**d) for d in data] + + @staticmethod + def transform_data(data: List[PolygonStockEODData]) -> List[StockEODData]: + return data_transformer(data, StockEODData) diff --git a/openbb_providers/builtin_providers/polygon/stock_news.py b/openbb_providers/builtin_providers/polygon/stock_news.py new file mode 100644 index 00000000000..666c28a9ef3 --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/stock_news.py @@ -0,0 +1,129 @@ +"""Polygon Stock News Fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Literal, Optional + +# IMPORT THIRD-PARTY +from pydantic import BaseModel, Field + +from builtin_providers.polygon.helpers import get_data + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams +from openbb_provider.model.data.stock_news import StockNewsData, StockNewsQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer, get_querystring + + +class PolygonStockNewsQueryParams(QueryParams): + """Polygon stock news query. + + Source: https://polygon.io/docs/stocks/get_v2_reference_news + + Parameters + ---------- + symbol : str + The symbol of the stocks to fetch. + ticker_lt : str, optional + Less than, by default None + ticker_lte : str, optional + Less than or equal, by default None + ticker_gt : str, optional + Greater than, by default None + ticker_gte : str, optional + Greater than or equal, by default None + published_utc : str, optional + The published date of the query, by default None + published_utc_lt : str, optional + Less than, by default None + published_utc_lte : str, optional + Less than or equal, by default None + published_utc_gt : str, optional + Greater than, by default None + published_utc_gte : str, optional + Greater than or equal, by default None + order : Literal["asc", "desc"], optional + The sort order of the query, by default None + limit : int, optional + The limit of the query, by default 100 + sort : str, optional + The sort of the query, by default None + """ + + __name__ = "PolygonStockNewsQueryParams" + ticker: str = Field(alias="symbols") + ticker_lt: Optional[str] = Field(alias="ticker.lt", default=None) + ticker_lte: Optional[str] = Field(alias="ticker.lte", default=None) + ticker_gt: Optional[str] = Field(alias="ticker.gt", default=None) + ticker_gte: Optional[str] = Field(alias="ticker.gte", default=None) + published_utc: Optional[str] = None + published_utc_lt: Optional[str] = Field(alias="published_utc.lt", default=None) + published_utc_lte: Optional[str] = Field(alias="published_utc.lte", default=None) + published_utc_gt: Optional[str] = Field(alias="published_utc.gt", default=None) + published_utc_gte: Optional[str] = Field(alias="published_utc.gte", default=None) + order: Optional[Literal["asc", "desc"]] = None + limit: Optional[int] = Field(default=100) + sort: Optional[str] = None + + +class PolygonPublisher(BaseModel): + favicon_url: str + homepage_url: str + logo_url: str + name: str + + +class PolygonStockNewsData(Data): + """Source: https://polygon.io/docs/stocks/get_v2_reference_news""" + + __name__ = "PolygonStockNewsData" + amp_url: Optional[str] + article_url: str = Field(alias="url") + author: Optional[str] + description: Optional[str] = Field(alias="text") + id: str + image_url: Optional[str] + keywords: Optional[List[str]] + published_utc: datetime = Field(alias="date") + publisher: PolygonPublisher = Field(alias="image") + tickers: List[str] + title: str + + +class PolygonStockNewsFetcher( + Fetcher[ + StockNewsQueryParams, + StockNewsData, + PolygonStockNewsQueryParams, + PolygonStockNewsData, + ] +): + @staticmethod + def transform_query( + query: StockNewsQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonStockNewsQueryParams: + return PolygonStockNewsQueryParams( + symbols=query.symbols, **extra_params if extra_params else {} + ) + + @staticmethod + def extract_data( + query: PolygonStockNewsQueryParams, api_key: str + ) -> List[PolygonStockNewsData]: + base_url = "https://api.polygon.io/v2/reference/news" + querystring = get_querystring(query.dict(), []) + request_url = f"{base_url}?{querystring}&apiKey={api_key}" + data = get_data(request_url)["results"] + + if len(data) == 0: + raise RuntimeError("No news found") + + return [PolygonStockNewsData(**d) for d in data] + + @staticmethod + def transform_data( + data: List[PolygonStockNewsData], + ) -> List[StockNewsData]: + processors = {"publisher": lambda x: x.favicon_url} + return data_transformer(data, StockNewsData, processors) diff --git a/openbb_providers/builtin_providers/polygon/stock_price.py b/openbb_providers/builtin_providers/polygon/stock_price.py new file mode 100644 index 00000000000..2544d049a28 --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/stock_price.py @@ -0,0 +1,97 @@ +"""Polygon stocks price fetcher.""" + +# IMPORT STANDARD +from datetime import datetime +from typing import Dict, List, Optional + +# IMPORT THIRD-PARTY +from pydantic import Field, NonNegativeFloat, PositiveFloat, PositiveInt + +from builtin_providers.polygon.helpers import get_data +from builtin_providers.polygon.types import BaseStockData, BaseStockQueryParams + +# IMPORT INTERNAL +from openbb_provider.model.data.stock_price import StockPriceData, StockPriceQueryParams +from openbb_provider.provider.abstract.fetcher import Fetcher +from openbb_provider.provider.provider_helpers import data_transformer + + +class PolygonStockPriceQueryParams(BaseStockQueryParams): + """Polygon stocks price query. + + Source: https://polygon.io/docs/stocks/getting-started + + Parameters + ---------- + symbol : str + The symbol of the stocks to fetch. + start_date : Union[date, datetime] + The start date of the query. + end_date : Union[date, datetime] + The end date of the query. + timespan : Timespan, optional + The timespan of the query, by default Timespan.day + sort : Literal["asc", "desc"], optional + The sort order of the query, by default "desc" + limit : PositiveInt, optional + The limit of the query, by default 49999 + adjusted : bool, optional + Whether the query is adjusted, by default True + multiplier : PositiveInt, optional + The multiplier of the query, by default 1 + """ + + __name__ = "PolygonStockPriceQueryParams" + + +class PolygonStockPriceData(BaseStockData): + __name__ = "PolygonPriceStockData" + v: NonNegativeFloat = Field(alias="volume") + n: PositiveInt + vw: PositiveFloat + + +class PolygonStockPriceFetcher( + Fetcher[ + StockPriceQueryParams, + StockPriceData, + PolygonStockPriceQueryParams, + PolygonStockPriceData, + ] +): + @staticmethod + def transform_query( + query: StockPriceQueryParams, extra_params: Optional[Dict] = None + ) -> PolygonStockPriceQueryParams: + return PolygonStockPriceQueryParams( + symbol=query.symbol, + start_date=query.start_date, + end_date=query.end_date if query.end_date else datetime.now(), + **extra_params if extra_params else {}, + ) + + @staticmethod + def extract_data( + query: PolygonStockPriceQueryParams, api_key: str + ) -> List[PolygonStockPriceData]: + request_url = ( + f"https://api.polygon.io/v2/aggs/ticker/" + f"{query.stocksTicker.upper()}/range/1/{query.timespan}/" + f"{query.start_date}/{query.end_date}?adjusted={query.adjusted}" + f"&sort={query.sort}&limit={query.limit}&multiplier={query.multiplier}" + f"&apiKey={api_key}" + ) + + data = get_data(request_url) + if isinstance(data, list): + raise ValueError("Expected a dict, got a list") + + if "results" not in data.keys() or len(data["results"]) == 0: + raise RuntimeError("No results found. Please change your query parameters.") + + data = data["results"] + return [PolygonStockPriceData(**d) for d in data] + + @staticmethod + def transform_data(data: List[PolygonStockPriceData]) -> List[StockPriceData]: + return data_transformer(data, StockPriceData) diff --git a/openbb_providers/builtin_providers/polygon/types.py b/openbb_providers/builtin_providers/polygon/types.py new file mode 100644 index 00000000000..0e36832ac27 --- /dev/null +++ b/openbb_providers/builtin_providers/polygon/types.py @@ -0,0 +1,68 @@ +"""Polygon types helpers.""" + +# IMPORT STANDARD +from datetime import date, datetime +from enum import Enum +from typing import Literal, Optional, Union + +# IMPORT THIRD-PARTY +from pydantic import Field, PositiveFloat, PositiveInt, validator + +# IMPORT INTERNAL +from openbb_provider.model.abstract.data import Data, QueryParams + + +class Timespan(str, Enum): + minute = "minute" + hour = "hour" + day = "day" + week = "week" + month = "month" + quarter = "quarter" + year = "year" + + +class PolygonFundamentalQueryParams(QueryParams): + ticker: Optional[str] = Field(alias="symbol") + cik: Optional[str] + company_name: Optional[str] + company_name_search: Optional[str] = Field(alias="company_name.search") + sic: Optional[str] + filing_date: Optional[date] + filing_date_lt: Optional[date] = Field(alias="filing_date.lt") + filing_date_lte: Optional[date] = Field(alias="filing_date.lte") + filing_date_gt: Optional[date] = Field(alias="filing_date.gt") + filing_date_gte: Optional[date] = Field(alias="filing_date.gte") + period_of_report_date: Optional[date] + period_of_report_date_lt: Optional[date] = Field(alias="period_of_report_date.lt") + period_of_report_date_lte: Optional[date] = Field(alias="period_of_report_date.lte") + period_of_report_date_gt: Optional[date] = Field(alias="period_of_report_date.gt") + period_of_report_date_gte: Optional[date] = Field(alias="period_of_report_date.gte") + timeframe: Optional[Literal["annual", "quarterly", "ttm"]] = Field(alias="period") + include_sources: Optional[bool] + order: Optional[Literal["asc", "desc"]] + limit: Optional[PositiveInt] = 10 + sort: Optional[Literal["filing_date", "period_of_report_date"]] + + +class BaseStockQueryParams(QueryParams): + stocksTicker: str = Field(alias="symbol") + start_date: Union[date, datetime] + end_date: Union[date, datetime] + timespan: Timespan = Field(default=Timespan.day) + sort: Literal["asc", "desc"] = Field(default="desc") + limit: PositiveInt = Field(default=49999) + adjusted: bool = Field(default=True) + multiplier: PositiveInt = Field(default=1) + + +class BaseStockData(Data): + c: PositiveFloat = Field(alias="close") + o: PositiveFloat = Field(alias="open") + h: PositiveFloat = Field(alias="high") + l: PositiveFloat = Field(alias="low") # noqa: E741 + t: datetime = Field(alias="date") + + @validator("t", pre=True) + def time_validate(cls, v): # pylint: disable=E0213 + return datetime.fromtimestamp(v / 1000) diff --git a/openbb_providers/openbb_provider/model/abstract/data.py b/openbb_providers/openbb_provider/model/abstract/data.py index 6aaca4f431f..a4d4879c708 100644 --- a/openbb_providers/openbb_provider/model/abstract/data.py +++ b/openbb_providers/openbb_provider/model/abstract/data.py @@ -14,6 +14,7 @@ class Data(BaseModel): class Config: extra = Extra.allow + allow_population_by_field_name = True class QueryParams(BaseModel): diff --git a/openbb_providers/openbb_provider/model/abstract/provider_data.py b/openbb_providers/openbb_provider/model/abstract/provider_data.py deleted file mode 100644 index 1766c62ca71..00000000000 --- a/openbb_providers/openbb_provider/model/abstract/provider_data.py +++ /dev/null @@ -1,34 +0,0 @@ -# IMPORT STANDARD - -# IMPORT THIRD-PARTY -from pydantic import BaseModel, Extra - -# IMPORT INTERNAL - - -class ProviderData(BaseModel): - """The Provider-specific data model.""" - - def __repr__(self): - return f"{self.__class__.__name__}({', '.join([f'{k}={v}' for k, v in self.dict().items()])})" - - class Config: - extra = Extra.allow - allow_population_by_field_name = True - - -class ProviderQueryParams(BaseModel): - """The Provider-specific query parameter model.""" - - def __post_init__(self): - # check the fields that were received vs the fields that are allowed - # if there are any fields that were received that are not allowed, raise an error - allowed_fields = set(self.__fields__.keys()) - received_fields = set(self.__dict__.keys()) - if not received_fields.issubset(allowed_fields): - raise ValueError( - f"Received fields that are not allowed: {received_fields - allowed_fields}" - ) - - def __repr__(self): - return f"{self.__class__.__name__}({', '.join([f'{k}={v}' for k, v in self.dict().items()])})" diff --git a/openbb_providers/openbb_provider/model/data/crypto_price.py b/openbb_providers/openbb_provider/model/data/crypto_price.py index 21ae86eec9e..f4d5dcb8f74 100644 --- a/openbb_providers/openbb_provider/model/data/crypto_price.py +++ b/openbb_providers/openbb_provider/model/data/crypto_price.py @@ -30,7 +30,7 @@ class CryptoPriceQueryParams(QueryParams, BaseSymbol): __name__ = "CryptoPriceQueryParams" # These fields only work with Polygon - start_date: Optional[date] = Field(description=DESCRIPTIONS.get("start_date", "")) + start_date: date = Field(description=DESCRIPTIONS.get("start_date", "")) end_date: Optional[date] = Field( description=DESCRIPTIONS.get("end_date", ""), default=date.today() ) diff --git a/openbb_providers/openbb_provider/provider/abstract/fetcher.py b/openbb_providers/openbb_provider/provider/abstract/fetcher.py index ccd8207ce35..479f4b83e44 100644 --- a/openbb_providers/openbb_provider/provider/abstract/fetcher.py +++ b/openbb_providers/openbb_provider/provider/abstract/fetcher.py @@ -6,15 +6,11 @@ from typing import Dict, Generic, List, Optional, TypeVar, Union # IMPORT THIRD-PARTY # IMPORT INTERNAL from openbb_provider.model.abstract.data import Data, QueryParams -from openbb_provider.model.abstract.provider_data import ( - ProviderData, - ProviderQueryParams, -) QueryParamsType = TypeVar("QueryParamsType", bound=QueryParams) DataType = TypeVar("DataType", bound=Data) -ProviderQueryParamsType = TypeVar("ProviderQueryParamsType", bound=ProviderQueryParams) -ProviderDataType = TypeVar("ProviderDataType", bound=ProviderData) +ProviderQueryParamsType = TypeVar("ProviderQueryParamsType", bound=QueryParams) +ProviderDataType = TypeVar("ProviderDataType", bound=Data) ReturnType = Union[List[ProviderDataType], ProviderDataType] UDataType = Union[List[DataType], DataType]