Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/fmp-stock-search: Add FMP as provider to obb.stocks.search() #5642

Merged
merged 3 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions openbb_platform/extensions/stocks/integration/test_stocks_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,29 @@ def test_stocks_multiples(params, headers):
[
({"query": "AAPl", "is_symbol": True, "provider": "cboe"}),
({"query": "Apple", "provider": "sec", "use_cache": False, "is_fund": False}),
(
{
"query": "residential",
"industry": "REIT",
"sector": "Real Estate",
"mktcap_min": None,
"mktcap_max": None,
"price_min": None,
"price_max": None,
"volume_min": None,
"volume_max": None,
"dividend_min": None,
"dividend_max": None,
"is_active": True,
"is_etf": False,
"beta_min": None,
"beta_max": None,
"country": "US",
"exchange": "nyse",
"limit": None,
"provider": "fmp",
}
),
],
)
@pytest.mark.integration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,29 @@ def test_stocks_multiples(params, obb):
[
({"query": "AAPL", "is_symbol": True, "provider": "cboe"}),
({"query": "Apple", "provider": "sec", "use_cache": False, "is_fund": False}),
(
{
"query": "residential",
"industry": "REIT",
"sector": "Real Estate",
"mktcap_min": None,
"mktcap_max": None,
"price_min": None,
"price_max": None,
"volume_min": None,
"volume_max": None,
"dividend_min": None,
"dividend_max": None,
"is_active": True,
"is_etf": False,
"beta_min": None,
"beta_max": None,
"country": "US",
"exchange": "nyse",
"limit": None,
"provider": "fmp",
}
),
],
)
@pytest.mark.integration
Expand Down
2 changes: 2 additions & 0 deletions openbb_platform/providers/fmp/openbb_fmp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from openbb_fmp.models.stock_ownership import FMPStockOwnershipFetcher
from openbb_fmp.models.stock_peers import FMPStockPeersFetcher
from openbb_fmp.models.stock_quote import FMPStockQuoteFetcher
from openbb_fmp.models.stock_search import FMPStockSearchFetcher
from openbb_fmp.models.stock_splits import FMPStockSplitCalendarFetcher
from openbb_fmp.models.treasury_rates import FMPTreasuryRatesFetcher
from openbb_provider.abstract.provider import Provider
Expand Down Expand Up @@ -112,5 +113,6 @@
"FinancialRatios": FMPFinancialRatiosFetcher,
"PricePerformance": FMPPricePerformanceFetcher,
"EconomicCalendar": FMPEconomicCalendarFetcher,
"StockSearch": FMPStockSearchFetcher,
},
)
205 changes: 205 additions & 0 deletions openbb_platform/providers/fmp/openbb_fmp/models/stock_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
"""FMP Stock Search fetcher."""

from typing import Any, Dict, List, Literal, Optional

import pandas as pd
from openbb_fmp.utils.definitions import EXCHANGES, SECTORS
from openbb_fmp.utils.helpers import create_url, get_data
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.stock_search import (
StockSearchData,
StockSearchQueryParams,
)
from pydantic import Field


class FMPStockSearchQueryParams(StockSearchQueryParams):
"""FMP Stock Search Query Params."""

__alias_dict__ = {
"mktcap_min": "marketCapMoreThan",
"mktcap_max": "marketCapLowerThan",
"price_min": "priceMoreThan",
"price_max": "priceLowerThan",
"beta_min": "betaMoreThan",
"beta_max": "betaLowerThan",
"volume_min": "volumeMoreThan",
"volume_max": "volumeLowerThan",
"dividend_min": "dividendMoreThan",
"dividend_max": "dividendLowerThan",
"is_active": "isActivelyTrading",
"is_etf": "isEtf",
}

mktcap_min: Optional[int] = Field(
default=None, description="Filter by market cap greater than this value."
)
mktcap_max: Optional[int] = Field(
default=None,
description="Filter by market cap less than this value.",
)
price_min: Optional[float] = Field(
default=None,
description="Filter by price greater than this value.",
)
price_max: Optional[float] = Field(
default=None,
description="Filter by price less than this value.",
)
beta_min: Optional[float] = Field(
default=None,
description="Filter by a beta greater than this value.",
)
beta_max: Optional[float] = Field(
default=None,
description="Filter by a beta less than this value.",
)
volume_min: Optional[int] = Field(
default=None,
description="Filter by volume greater than this value.",
)
volume_max: Optional[int] = Field(
default=None,
description="Filter by volume less than this value.",
)
dividend_min: Optional[float] = Field(
default=None,
description="Filter by dividend amount greater than this value.",
)
dividend_max: Optional[float] = Field(
default=None,
description="Filter by dividend amount less than this value.",
)
is_etf: Optional[bool] = Field(
default=False,
description="If true, returns only ETFs.",
)
is_active: Optional[bool] = Field(
default=True,
description="If false, returns only inactive tickers.",
)
sector: Optional[SECTORS] = Field(default=None, description="Filter by sector.")
industry: Optional[str] = Field(default=None, description="Filter by industry.")
country: Optional[str] = Field(
default=None, description="Filter by country, as a two-letter country code."
)
exchange: Optional[EXCHANGES] = Field(
default=None, description="Filter by exchange."
)
limit: Optional[int] = Field(
default=50000, description="Limit the number of results to return."
)


class FMPStockSearchData(StockSearchData):
"""FMP Stock Search Data."""

__alias_dict__ = {
"name": "companyName",
}

market_cap: Optional[int] = Field(
description="The market cap of ticker.", alias="marketCap", default=None
)
sector: Optional[str] = Field(
description="The sector the ticker belongs to.", default=None
)
industry: Optional[str] = Field(
description="The industry ticker belongs to.", default=None
)
beta: Optional[float] = Field(description="The beta of the ETF.", default=None)
price: Optional[float] = Field(description="The current price.", default=None)
last_annual_dividend: Optional[float] = Field(
description="The last annual amount dividend paid.",
alias="lastAnnualDividend",
default=None,
)
volume: Optional[int] = Field(
description="The current trading volume.", default=None
)
exchange: Optional[str] = Field(
description="The exchange code the asset trades on.",
alias="exchangeShortName",
default=None,
)
exchange_name: Optional[str] = Field(
description="The full name of the primary exchange.",
alias="exchange",
default=None,
)
country: Optional[str] = Field(
description="The two-letter country abbreviation where the head office is located.",
default=None,
)
is_etf: Optional[Literal[True, False]] = Field(
description="Whether the ticker is an ETF.", alias="isEtf", default=None
)
actively_trading: Optional[Literal[True, False]] = Field(
description="Whether the ETF is actively trading.",
alias="isActivelyTrading",
default=None,
)


class FMPStockSearchFetcher(
Fetcher[
FMPStockSearchQueryParams,
List[FMPStockSearchData],
]
):
"""Transform the query, extract and transform the data from the FMP endpoints."""

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

@staticmethod
def extract_data(
query: FMPStockSearchQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the FMP endpoint."""
api_key = credentials.get("fmp_api_key") if credentials else ""
url = create_url(
version=3,
endpoint="stock-screener",
api_key=api_key,
query=query,
exclude=["query", "is_symbol", "industry"],
).replace(" ", "%20")
return get_data(url, **kwargs)

@staticmethod
def transform_data(
query: FMPStockSearchQueryParams, data: List[Dict], **kwargs: Any
) -> List[FMPStockSearchData]:
"""Return the transformed data."""
results = pd.DataFrame(data)
if len(results) == 0:
return []
if query.industry:
results = results[
results["sector"].str.contains(query.industry, case=False)
| results["industry"].str.contains(query.industry, case=False)
]
if query.query:
results = results[
results["companyName"].str.contains(query.query, case=False)
| results["exchangeShortName"].str.contains(query.query, case=False)
| results["exchange"].str.contains(query.query, case=False)
| results["sector"].str.contains(query.query, case=False)
| results["industry"].str.contains(query.query, case=False)
| results["country"].str.contains(query.query, case=False)
]
results["companyName"] = results["companyName"].fillna("-").replace("-", "")
for col in results:
if results[col].dtype in ("int", "float"):
results[col] = results[col].fillna(0).replace(0, None)
return [
FMPStockSearchData.model_validate(d)
for d in results.sort_values(by="marketCap", ascending=False).to_dict(
"records"
)
]
78 changes: 78 additions & 0 deletions openbb_platform/providers/fmp/openbb_fmp/utils/definitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""FMP Definitions"""

from typing import Literal

SECTORS = Literal[
"Consumer Cyclical",
"Energy",
"Technology",
"Industrials",
"Financial Services",
"Basic Materials",
"Communication Services",
"Consumer Defensive",
"Healthcare",
"Real Estate",
"Utilities",
"Industrial Goods",
"Financial",
"Services",
"Conglomerates",
]

EXCHANGES = Literal[
"amex",
"ase",
"asx",
"ath",
"bme",
"bru",
"bud",
"bue",
"cai",
"cnq",
"cph",
"dfm",
"doh",
"etf",
"euronext",
"hel",
"hkse",
"ice",
"iob",
"ist",
"jkt",
"jnb",
"jpx",
"kls",
"koe",
"ksc",
"kuw",
"lse",
"mex",
"nasdaq",
"neo",
"nse",
"nyse",
"nze",
"osl",
"otc",
"pnk",
"pra",
"ris",
"sao",
"sau",
"set",
"sgo",
"shh",
"shz",
"six",
"sto",
"tai",
"tlv",
"tsx",
"two",
"vie",
"wse",
"xetra",
]
Loading
Loading