mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
feat(platform): extend Country type standardization to equity screeners (#7355)
* feat(platform): extend Country type standardization to equity screeners Apply standardized Country type from PR #7333 to additional providers. Changes: - YFinance equity_screener: Accept ISO codes, country names; convert to lowercase - Nasdaq equity_screener: Accept ISO codes, preserve multi-value functionality - Nasdaq economic_calendar: Accept ISO codes, convert to snake_case - Seeking Alpha calendar_earnings: Accept ISO codes for US/CA All validators normalize inputs to provider-expected formats while maintaining backward compatibility with existing inputs. Part of extending #6969 coverage across providers. --- Question for maintainers: Should this be extended to moderate (OECD, EconDB) or full (all providers) coverage? * refactor: apply review feedback - validate invalid country input instead of passing to API * refactor(yfinance): validate country input against supported list * dedupe nasdaq screener choices definitions * fix(nasdaq): use get_args() instead of __args__ for mypy compatibility --------- Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>
This commit is contained in:
@@ -9,7 +9,9 @@ from openbb_core.provider.standard_models.etf_search import (
|
||||
EtfSearchData,
|
||||
EtfSearchQueryParams,
|
||||
)
|
||||
from pydantic import ConfigDict, Field
|
||||
from openbb_core.provider.utils.country_utils import Country
|
||||
from openbb_fmp.utils.definitions import Countries
|
||||
from pydantic import ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class FMPEtfSearchQueryParams(EtfSearchQueryParams):
|
||||
@@ -26,13 +28,42 @@ class FMPEtfSearchQueryParams(EtfSearchQueryParams):
|
||||
{"label": "Euronext", "value": "euronext"},
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"country": {
|
||||
"x-widget_config": {
|
||||
"options": [
|
||||
{"label": country.upper(), "value": country}
|
||||
for country in Countries.__args__
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
exchange: Literal["amex", "nyse", "nasdaq", "tsx", "euronext"] | None = Field(
|
||||
description="Exchange where the ETF is listed. If not provided, all exchanges are searched.",
|
||||
default=None,
|
||||
)
|
||||
country: Country | None = Field(
|
||||
default=None,
|
||||
description="Filter by country. Accepts ISO 3166-1 alpha-2 codes (e.g., 'US', 'DE'), "
|
||||
"alpha-3 codes (e.g., 'USA'), or country names (e.g., 'United States', 'united_states').",
|
||||
)
|
||||
|
||||
@field_validator("country", mode="after")
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country is supported by FMP."""
|
||||
if v is None:
|
||||
return v
|
||||
# Country stores alpha_2 in uppercase, FMP expects lowercase
|
||||
country_code = v.alpha_2.lower()
|
||||
valid_countries = list(Countries.__args__)
|
||||
if country_code not in valid_countries:
|
||||
raise ValueError(
|
||||
f"Country '{v.name}' ({v.alpha_2}) is not supported by FMP. "
|
||||
f"Valid options: {', '.join(sorted(valid_countries)[:20])}..."
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class FMPEtfSearchData(EtfSearchData):
|
||||
@@ -100,6 +131,8 @@ class FMPEtfSearchFetcher(
|
||||
url = "https://financialmodelingprep.com/stable/company-screener?isEtf=true&isFund=false&isActivelyTrading=true"
|
||||
if query.exchange:
|
||||
url += f"&exchange={query.exchange.upper()}"
|
||||
if query.country:
|
||||
url += f"&country={query.country.upper()}"
|
||||
|
||||
url += f"&limit=10000&apikey={api_key}"
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from openbb_core.provider.standard_models.economic_calendar import (
|
||||
EconomicCalendarData,
|
||||
EconomicCalendarQueryParams,
|
||||
)
|
||||
from openbb_core.provider.utils.country_utils import Country
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
@@ -28,14 +29,27 @@ class NasdaqEconomicCalendarQueryParams(EconomicCalendarQueryParams):
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country of the event",
|
||||
description="Country of the event. Accepts country names, "
|
||||
"ISO 3166-1 alpha-2/alpha-3 codes. Multiple comma-separated values allowed.",
|
||||
)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def validate_country(cls, c: str): # pylint: disable=E0213
|
||||
"""Validate country."""
|
||||
return ",".join([v.lower() for v in c.replace(" ", "_").split(",")])
|
||||
"""Validate and normalize country input."""
|
||||
if c is None:
|
||||
return c
|
||||
results = []
|
||||
for item in c.split(","):
|
||||
stripped = item.strip()
|
||||
# Try to convert via Country type
|
||||
try:
|
||||
country = Country(stripped)
|
||||
results.append(country.name.lower().replace(" ", "_"))
|
||||
except ValueError:
|
||||
# Keep original format (lowercase snake_case)
|
||||
results.append(stripped.lower().replace(" ", "_"))
|
||||
return ",".join(results)
|
||||
|
||||
|
||||
class NasdaqEconomicCalendarData(EconomicCalendarData):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, get_args
|
||||
from warnings import warn
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
@@ -11,14 +11,17 @@ from openbb_core.provider.standard_models.equity_screener import (
|
||||
EquityScreenerData,
|
||||
EquityScreenerQueryParams,
|
||||
)
|
||||
from openbb_core.provider.utils.country_utils import Country
|
||||
from openbb_core.provider.utils.errors import EmptyDataError
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
EXCHANGE_CHOICES = ["all", "nasdaq", "nyse", "amex"]
|
||||
EXSUBCATEGORY_CHOICES = ["all", "ngs", "ngm", "ncm", "adr"]
|
||||
MKT_CAP_CHOICES = ["all", "mega", "large", "mid", "small", "micro"]
|
||||
RECOMMENDATION_CHOICES = ["all", "strong_buy", "buy", "hold", "sell", "strong_sell"]
|
||||
SECTOR_CHOICES = [
|
||||
EXCHANGE_CHOICES = Literal["all", "nasdaq", "nyse", "amex"]
|
||||
EXSUBCATEGORY_CHOICES = Literal["all", "ngs", "ngm", "ncm", "adr"]
|
||||
MKT_CAP_CHOICES = Literal["all", "mega", "large", "mid", "small", "micro"]
|
||||
RECOMMENDATION_CHOICES = Literal[
|
||||
"all", "strong_buy", "buy", "hold", "sell", "strong_sell"
|
||||
]
|
||||
SECTOR_CHOICES = Literal[
|
||||
"all",
|
||||
"energy",
|
||||
"basic_materials",
|
||||
@@ -32,7 +35,7 @@ SECTOR_CHOICES = [
|
||||
"utilities",
|
||||
"real_estate",
|
||||
]
|
||||
REGION_CHOICES = [
|
||||
REGION_CHOICES = Literal[
|
||||
"all",
|
||||
"africa",
|
||||
"asia",
|
||||
@@ -43,7 +46,7 @@ REGION_CHOICES = [
|
||||
"north_america",
|
||||
"south_america",
|
||||
]
|
||||
COUNTRY_CHOICES = [
|
||||
COUNTRY_CHOICES = Literal[
|
||||
"all",
|
||||
"argentina",
|
||||
"armenia",
|
||||
@@ -106,145 +109,73 @@ class NasdaqEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
"mktcap": "marketcap",
|
||||
}
|
||||
__json_schema_extra__ = {
|
||||
"exchange": {"multiple_items_allowed": True},
|
||||
"exsubcategory": {"multiple_items_allowed": True},
|
||||
"mktcap": {"multiple_items_allowed": True},
|
||||
"recommendation": {"multiple_items_allowed": True},
|
||||
"sector": {"multiple_items_allowed": True},
|
||||
"region": {"multiple_items_allowed": True},
|
||||
"country": {"multiple_items_allowed": True},
|
||||
"exchange": {
|
||||
"multiple_items_allowed": True,
|
||||
"choices": list(get_args(EXCHANGE_CHOICES)),
|
||||
},
|
||||
"exsubcategory": {
|
||||
"multiple_items_allowed": True,
|
||||
"choices": list(get_args(EXSUBCATEGORY_CHOICES)),
|
||||
},
|
||||
"mktcap": {
|
||||
"multiple_items_allowed": True,
|
||||
"choices": list(get_args(MKT_CAP_CHOICES)),
|
||||
},
|
||||
"recommendation": {
|
||||
"multiple_items_allowed": True,
|
||||
"choices": list(get_args(RECOMMENDATION_CHOICES)),
|
||||
},
|
||||
"sector": {
|
||||
"multiple_items_allowed": True,
|
||||
"choices": list(get_args(SECTOR_CHOICES)),
|
||||
},
|
||||
"region": {
|
||||
"multiple_items_allowed": True,
|
||||
"choices": list(get_args(REGION_CHOICES)),
|
||||
},
|
||||
"country": {
|
||||
"multiple_items_allowed": True,
|
||||
"choices": list(get_args(COUNTRY_CHOICES)),
|
||||
},
|
||||
}
|
||||
|
||||
exchange: Literal["all", "nasdaq", "nyse", "amex"] | str = Field(
|
||||
exchange: EXCHANGE_CHOICES | str = Field(
|
||||
default="all",
|
||||
description="Filter by exchange.",
|
||||
json_schema_extra={"choices": EXCHANGE_CHOICES},
|
||||
)
|
||||
exsubcategory: Literal["all", "ngs", "ngm", "ncm", "adr"] | str = Field(
|
||||
exsubcategory: EXSUBCATEGORY_CHOICES | str = Field(
|
||||
default="all",
|
||||
description="Filter by exchange subcategory."
|
||||
"\n NGS - Nasdaq Global Select Market"
|
||||
"\n NGM - Nasdaq Global Market"
|
||||
"\n NCM - Nasdaq Capital Market"
|
||||
"\n ADR - American Depository Receipt\n",
|
||||
json_schema_extra={"choices": EXSUBCATEGORY_CHOICES},
|
||||
"\n- NGS - Nasdaq Global Select Market"
|
||||
"\n- NGM - Nasdaq Global Market"
|
||||
"\n- NCM - Nasdaq Capital Market"
|
||||
"\n- ADR - American Depository Receipt\n",
|
||||
)
|
||||
mktcap: Literal["all", "mega", "large", "mid", "small", "micro"] | str = Field(
|
||||
mktcap: MKT_CAP_CHOICES | str = Field(
|
||||
default="all",
|
||||
description="Filter by market cap."
|
||||
"\n Mega - > 200B"
|
||||
"\n Large - 10B - 200B"
|
||||
"\n Mid - 2B - 10B"
|
||||
"\n Small - 300M - 2B"
|
||||
"\n Micro - 50M - 300M\n",
|
||||
json_schema_extra={"choices": MKT_CAP_CHOICES},
|
||||
"\n- Mega - > 200B"
|
||||
"\n- Large - 10B - 200B"
|
||||
"\n- Mid - 2B - 10B"
|
||||
"\n- Small - 300M - 2B"
|
||||
"\n- Micro - 50M - 300M\n",
|
||||
)
|
||||
recommendation: (
|
||||
Literal["all", "strong_buy", "buy", "hold", "sell", "strong_sell"] | str
|
||||
) = Field(
|
||||
recommendation: RECOMMENDATION_CHOICES | str = Field(
|
||||
default="all",
|
||||
description="Filter by consensus analyst action.",
|
||||
json_schema_extra={"choices": RECOMMENDATION_CHOICES},
|
||||
)
|
||||
sector: (
|
||||
Literal[
|
||||
"all",
|
||||
"energy",
|
||||
"basic_materials",
|
||||
"industrials",
|
||||
"consumer_staples",
|
||||
"consumer_discretionary",
|
||||
"health_care",
|
||||
"financial_services",
|
||||
"technology",
|
||||
"communication_services",
|
||||
"utilities",
|
||||
"real_estate",
|
||||
]
|
||||
| str
|
||||
) = Field(
|
||||
sector: SECTOR_CHOICES | str = Field(
|
||||
default="all",
|
||||
description="Filter by sector.",
|
||||
json_schema_extra={"choices": SECTOR_CHOICES},
|
||||
)
|
||||
region: (
|
||||
Literal[
|
||||
"all",
|
||||
"africa",
|
||||
"asia",
|
||||
"australia_and_south_pacific",
|
||||
"caribbean",
|
||||
"europe",
|
||||
"middle_east",
|
||||
"north_america",
|
||||
"south_america",
|
||||
]
|
||||
| str
|
||||
) = Field(
|
||||
region: REGION_CHOICES | str = Field(
|
||||
default="all",
|
||||
description="Filter by region.",
|
||||
json_schema_extra={"choices": REGION_CHOICES},
|
||||
)
|
||||
country: (
|
||||
Literal[
|
||||
"all",
|
||||
"argentina",
|
||||
"armenia",
|
||||
"australia",
|
||||
"austria",
|
||||
"belgium",
|
||||
"bermuda",
|
||||
"brazil",
|
||||
"canada",
|
||||
"cayman_islands",
|
||||
"chile",
|
||||
"colombia",
|
||||
"costa_rica",
|
||||
"curacao",
|
||||
"cyprus",
|
||||
"denmark",
|
||||
"finland",
|
||||
"france",
|
||||
"germany",
|
||||
"greece",
|
||||
"guernsey",
|
||||
"hong_kong",
|
||||
"india",
|
||||
"indonesia",
|
||||
"ireland",
|
||||
"isle_of_man",
|
||||
"israel",
|
||||
"italy",
|
||||
"japan",
|
||||
"jersey",
|
||||
"luxembourg",
|
||||
"macau",
|
||||
"mexico",
|
||||
"monaco",
|
||||
"netherlands",
|
||||
"norway",
|
||||
"panama",
|
||||
"peru",
|
||||
"philippines",
|
||||
"puerto_rico",
|
||||
"russia",
|
||||
"singapore",
|
||||
"south_africa",
|
||||
"south_korea",
|
||||
"spain",
|
||||
"sweden",
|
||||
"switzerland",
|
||||
"taiwan",
|
||||
"turkey",
|
||||
"united_kingdom",
|
||||
"united_states",
|
||||
"usa",
|
||||
]
|
||||
| str
|
||||
) = Field(
|
||||
country: COUNTRY_CHOICES | str = Field(
|
||||
default="all",
|
||||
description="Filter by country.",
|
||||
json_schema_extra={"choices": COUNTRY_CHOICES},
|
||||
description="Filter by country. Accepts country names, ISO 3166-1 alpha-2/alpha-3 codes, "
|
||||
"or 'all' for all countries. Multiple comma-separated values allowed.",
|
||||
)
|
||||
limit: int | None = Field(
|
||||
default=None,
|
||||
@@ -260,7 +191,7 @@ class NasdaqEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
for item in v:
|
||||
if item == "all":
|
||||
continue
|
||||
if item in EXCHANGE_CHOICES:
|
||||
if item in list(get_args(EXCHANGE_CHOICES)):
|
||||
new_items.append(item)
|
||||
else:
|
||||
warn(f"Invalid exchange: {item}")
|
||||
@@ -275,7 +206,7 @@ class NasdaqEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
for item in v:
|
||||
if item == "all":
|
||||
continue
|
||||
if item in EXSUBCATEGORY_CHOICES:
|
||||
if item in list(get_args(EXSUBCATEGORY_CHOICES)):
|
||||
new_items.append(item)
|
||||
else:
|
||||
warn(f"Invalid exsubcategory: {item}")
|
||||
@@ -290,7 +221,7 @@ class NasdaqEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
for item in v:
|
||||
if item == "all":
|
||||
continue
|
||||
if item in MKT_CAP_CHOICES:
|
||||
if item in list(get_args(MKT_CAP_CHOICES)):
|
||||
new_items.append(item)
|
||||
else:
|
||||
warn(f"Invalid market cap: {item}")
|
||||
@@ -305,7 +236,7 @@ class NasdaqEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
for item in v:
|
||||
if item == "all":
|
||||
continue
|
||||
if item in RECOMMENDATION_CHOICES:
|
||||
if item in list(get_args(RECOMMENDATION_CHOICES)):
|
||||
new_items.append(item)
|
||||
else:
|
||||
warn(f"Invalid recommendation: {item}")
|
||||
@@ -320,7 +251,7 @@ class NasdaqEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
for item in v:
|
||||
if item == "all":
|
||||
continue
|
||||
if item in SECTOR_CHOICES:
|
||||
if item in list(get_args(SECTOR_CHOICES)):
|
||||
new_items.append(item)
|
||||
else:
|
||||
warn(f"Invalid sector: {item}")
|
||||
@@ -335,7 +266,7 @@ class NasdaqEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
for item in v:
|
||||
if item == "all":
|
||||
continue
|
||||
if item in REGION_CHOICES:
|
||||
if item in list(get_args(REGION_CHOICES)):
|
||||
new_items.append(item)
|
||||
else:
|
||||
warn(f"Invalid region: {item}")
|
||||
@@ -344,14 +275,31 @@ class NasdaqEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
"""Validate country.
|
||||
|
||||
Accepts Country type, ISO codes, country names, or snake_case names.
|
||||
Converts all inputs to Nasdaq's expected snake_case format.
|
||||
"""
|
||||
if isinstance(v, Country):
|
||||
# Convert Country to snake_case name
|
||||
v = v.name.lower().replace(" ", "_").replace("-", "_")
|
||||
v = v.split(",")
|
||||
new_items = []
|
||||
for item in v:
|
||||
if item == "all":
|
||||
continue
|
||||
if item in COUNTRY_CHOICES:
|
||||
new_items.append(item)
|
||||
# Try to convert via Country type if not already valid
|
||||
normalized_item = item
|
||||
if item not in list(get_args(COUNTRY_CHOICES)):
|
||||
try:
|
||||
country = Country(item)
|
||||
normalized_item = (
|
||||
country.name.lower().replace(" ", "_").replace("-", "_")
|
||||
)
|
||||
except ValueError:
|
||||
pass # Keep original, will warn below
|
||||
if normalized_item in list(get_args(COUNTRY_CHOICES)):
|
||||
new_items.append(normalized_item)
|
||||
else:
|
||||
warn(f"Invalid country: {item}")
|
||||
return ",".join(new_items) if new_items else "all"
|
||||
@@ -408,14 +356,14 @@ class NasdaqEquityScreenerData(EquityScreenerData):
|
||||
.replace("--", "")
|
||||
.replace("NA", "")
|
||||
)
|
||||
return v if v else None
|
||||
return v or None
|
||||
|
||||
|
||||
class NasdaqEquityScreenerFetcher(
|
||||
Fetcher[
|
||||
NasdaqEquityScreenerQueryParams,
|
||||
list[NasdaqEquityScreenerData],
|
||||
]
|
||||
],
|
||||
):
|
||||
"""Nasdaq Equity Screener Fetcher."""
|
||||
|
||||
@@ -440,7 +388,7 @@ class NasdaqEquityScreenerFetcher(
|
||||
HEADERS = get_headers(accept_type="text")
|
||||
base_url = (
|
||||
"https://api.nasdaq.com/api/screener/stocks?tableonly=true&limit="
|
||||
+ f"{query.limit if query.limit else 10000}&"
|
||||
f"{query.limit or 10000}&"
|
||||
)
|
||||
exchange = query.exchange.split(",")
|
||||
exsubcategory = query.exsubcategory.split(",")
|
||||
|
||||
@@ -14,9 +14,12 @@ from openbb_core.provider.standard_models.calendar_earnings import (
|
||||
CalendarEarningsData,
|
||||
CalendarEarningsQueryParams,
|
||||
)
|
||||
from openbb_core.provider.utils.country_utils import Country
|
||||
from openbb_seeking_alpha.utils.helpers import HEADERS, date_range
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
SA_COUNTRIES = {"us", "ca"}
|
||||
|
||||
|
||||
class SACalendarEarningsQueryParams(CalendarEarningsQueryParams):
|
||||
"""Seeking Alpha Calendar Earnings Query.
|
||||
@@ -26,10 +29,29 @@ class SACalendarEarningsQueryParams(CalendarEarningsQueryParams):
|
||||
|
||||
country: Literal["us", "ca"] = Field(
|
||||
default="us",
|
||||
description="The country to get calendar data for.",
|
||||
description="The country to get calendar data for. "
|
||||
"Accepts 'us'/'ca', ISO codes ('US', 'USA', 'CA', 'CAN'), "
|
||||
"or names ('United States', 'Canada').",
|
||||
json_schema_extra={"choices": ["us", "ca"]},
|
||||
)
|
||||
|
||||
@field_validator("country", mode="before")
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Normalize country input to Seeking Alpha format."""
|
||||
if v in SA_COUNTRIES:
|
||||
return v
|
||||
try:
|
||||
country = Country(v)
|
||||
code = country.alpha_2.lower()
|
||||
if code in SA_COUNTRIES:
|
||||
return code
|
||||
except ValueError:
|
||||
pass
|
||||
raise ValueError(
|
||||
f"Country '{v}' not supported. Seeking Alpha only supports US and Canada."
|
||||
)
|
||||
|
||||
|
||||
class SACalendarEarningsData(CalendarEarningsData):
|
||||
"""Seeking Alpha Calendar Earnings Data."""
|
||||
|
||||
@@ -10,6 +10,7 @@ from openbb_core.provider.standard_models.equity_screener import (
|
||||
EquityScreenerData,
|
||||
EquityScreenerQueryParams,
|
||||
)
|
||||
from openbb_core.provider.utils.country_utils import Country
|
||||
from openbb_core.provider.utils.errors import EmptyDataError
|
||||
from openbb_yfinance.utils.references import (
|
||||
COUNTRIES,
|
||||
@@ -23,7 +24,7 @@ from openbb_yfinance.utils.references import (
|
||||
YFPredefinedScreenerData,
|
||||
get_industry_sector,
|
||||
)
|
||||
from pydantic import Field
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class YFinanceEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
@@ -50,7 +51,8 @@ class YFinanceEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
|
||||
country: str | None = Field(
|
||||
default="us",
|
||||
description="Filter by country, as a two-letter country code. Default is, 'us'. Use, 'all', for all countries.",
|
||||
description="Filter by country. Accepts ISO 3166-1 alpha-2 codes (e.g., 'US', 'DE'), "
|
||||
"alpha-3 codes (e.g., 'USA'), country names (e.g., 'United States'), or 'all' for all countries.",
|
||||
)
|
||||
exchange: Exchanges | None = Field(
|
||||
default=None,
|
||||
@@ -98,13 +100,34 @@ class YFinanceEquityScreenerQueryParams(EquityScreenerQueryParams):
|
||||
description="Limit the number of results returned. Default is, 200. Set to, 0, for all results.",
|
||||
)
|
||||
|
||||
@field_validator("country", mode="before")
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate and normalize country input."""
|
||||
if v is None or v == "all":
|
||||
return v
|
||||
# Convert Country type or string to lowercase alpha-2 for YFinance
|
||||
if isinstance(v, Country):
|
||||
country_code = v.alpha_2.lower()
|
||||
else:
|
||||
try:
|
||||
country_code = Country(v).alpha_2.lower()
|
||||
except ValueError:
|
||||
country_code = v.strip().lower()
|
||||
if country_code not in COUNTRIES:
|
||||
raise ValueError(
|
||||
f"Country '{v}' ({country_code.upper()}) is not supported by YFinance. "
|
||||
f"Valid options: {', '.join(sorted(COUNTRIES))}",
|
||||
)
|
||||
return country_code
|
||||
|
||||
|
||||
class YFinanceEquityScreenerData(EquityScreenerData, YFPredefinedScreenerData):
|
||||
"""YFinance Equity Screener Data."""
|
||||
|
||||
|
||||
class YFinanceEquityScreenerFetcher(
|
||||
Fetcher[YFinanceEquityScreenerQueryParams, list[YFinanceEquityScreenerData]]
|
||||
Fetcher[YFinanceEquityScreenerQueryParams, list[YFinanceEquityScreenerData]],
|
||||
):
|
||||
"""YFinance Equity Screener Fetcher."""
|
||||
|
||||
@@ -121,8 +144,8 @@ class YFinanceEquityScreenerFetcher(
|
||||
raise OpenBBError(
|
||||
ValueError(
|
||||
f"Industry {industry} does not belong to sector {sector}."
|
||||
" Valid choices are:" + "\n\n " + f"{choices}"
|
||||
)
|
||||
" Valid choices are:" + "\n\n " + f"{choices}",
|
||||
),
|
||||
)
|
||||
elif industry and not sector:
|
||||
choices = "\n".join(INDUSTRIES)
|
||||
@@ -131,8 +154,8 @@ class YFinanceEquityScreenerFetcher(
|
||||
raise OpenBBError(
|
||||
ValueError(
|
||||
f"Industry {industry} not found. Valid choices are:\n"
|
||||
+ f"{choices}"
|
||||
)
|
||||
f"{choices}",
|
||||
),
|
||||
)
|
||||
_industry = INDUSTRY_MAP[sector][industry]
|
||||
|
||||
@@ -155,7 +178,7 @@ class YFinanceEquityScreenerFetcher(
|
||||
|
||||
if query.exchange is not None:
|
||||
operands.append(
|
||||
{"operator": "eq", "operands": ["exchange", query.exchange.upper()]}
|
||||
{"operator": "eq", "operands": ["exchange", query.exchange.upper()]},
|
||||
)
|
||||
query.country = "all"
|
||||
|
||||
@@ -175,39 +198,39 @@ class YFinanceEquityScreenerFetcher(
|
||||
industry = INDUSTRY_MAP[sector][query.industry]
|
||||
if industry in PEER_GROUPS:
|
||||
operands.append(
|
||||
{"operator": "EQ", "operands": ["peer_group", industry]}
|
||||
{"operator": "EQ", "operands": ["peer_group", industry]},
|
||||
)
|
||||
else:
|
||||
operands.append({"operator": "EQ", "operands": ["industry", industry]})
|
||||
|
||||
if query.mktcap_min is not None:
|
||||
operands.append(
|
||||
{"operator": "gt", "operands": ["intradaymarketcap", query.mktcap_min]}
|
||||
{"operator": "gt", "operands": ["intradaymarketcap", query.mktcap_min]},
|
||||
)
|
||||
|
||||
if query.mktcap_max is not None:
|
||||
operands.append(
|
||||
{"operator": "lt", "operands": ["intradaymarketcap", query.mktcap_max]}
|
||||
{"operator": "lt", "operands": ["intradaymarketcap", query.mktcap_max]},
|
||||
)
|
||||
|
||||
if query.price_min is not None:
|
||||
operands.append(
|
||||
{"operator": "gt", "operands": ["intradayprice", query.price_min]}
|
||||
{"operator": "gt", "operands": ["intradayprice", query.price_min]},
|
||||
)
|
||||
|
||||
if query.price_max is not None:
|
||||
operands.append(
|
||||
{"operator": "lt", "operands": ["intradayprice", query.price_max]}
|
||||
{"operator": "lt", "operands": ["intradayprice", query.price_max]},
|
||||
)
|
||||
|
||||
if query.volume_min is not None:
|
||||
operands.append(
|
||||
{"operator": "gt", "operands": ["dayvolume", query.volume_min]}
|
||||
{"operator": "gt", "operands": ["dayvolume", query.volume_min]},
|
||||
)
|
||||
|
||||
if query.volume_max is not None:
|
||||
operands.append(
|
||||
{"operator": "lt", "operands": ["dayvolume", query.volume_max]}
|
||||
{"operator": "lt", "operands": ["dayvolume", query.volume_max]},
|
||||
)
|
||||
|
||||
if query.beta_min is not None:
|
||||
|
||||
Reference in New Issue
Block a user