From 2a8a1daa32f718a41bed127515072b9cd0a67833 Mon Sep 17 00:00:00 2001 From: R5dan Date: Thu, 30 Jan 2025 12:39:30 +0000 Subject: [PATCH 1/3] Screener --- yfinance/const.py | 506 +--------------------------------- yfinance/screener/__init__.py | 6 +- yfinance/screener/const.py | 494 +++++++++++++++++++++++++++++++++ yfinance/screener/query.py | 461 +++++++++++++++++++++++++++---- yfinance/screener/screener.py | 180 ------------ yfinance/utils.py | 21 ++ 6 files changed, 926 insertions(+), 742 deletions(-) create mode 100644 yfinance/screener/const.py delete mode 100644 yfinance/screener/screener.py diff --git a/yfinance/const.py b/yfinance/const.py index b4a1fd3c7..55af6660a 100644 --- a/yfinance/const.py +++ b/yfinance/const.py @@ -119,508 +119,4 @@ "PaymentstoSuppliersforGoodsandServices", "ClassesofCashReceiptsfromOperatingActivities", "OtherCashReceiptsfromOperatingActivities", "ReceiptsfromGovernmentGrants", "ReceiptsfromCustomers"]} -_PRICE_COLNAMES_ = ['Open', 'High', 'Low', 'Close', 'Adj Close'] - -quote_summary_valid_modules = ( - "summaryProfile", # contains general information about the company - "summaryDetail", # prices + volume + market cap + etc - "assetProfile", # summaryProfile + company officers - "fundProfile", - "price", # current prices - "quoteType", # quoteType - "esgScores", # Environmental, social, and governance (ESG) scores, sustainability and ethical performance of companies - "incomeStatementHistory", - "incomeStatementHistoryQuarterly", - "balanceSheetHistory", - "balanceSheetHistoryQuarterly", - "cashFlowStatementHistory", - "cashFlowStatementHistoryQuarterly", - "defaultKeyStatistics", # KPIs (PE, enterprise value, EPS, EBITA, and more) - "financialData", # Financial KPIs (revenue, gross margins, operating cash flow, free cash flow, and more) - "calendarEvents", # future earnings date - "secFilings", # SEC filings, such as 10K and 10Q reports - "upgradeDowngradeHistory", # upgrades and downgrades that analysts have given a company's stock - "institutionOwnership", # institutional ownership, holders and shares outstanding - "fundOwnership", # mutual fund ownership, holders and shares outstanding - "majorDirectHolders", - "majorHoldersBreakdown", - "insiderTransactions", # insider transactions, such as the number of shares bought and sold by company executives - "insiderHolders", # insider holders, such as the number of shares held by company executives - "netSharePurchaseActivity", # net share purchase activity, such as the number of shares bought and sold by company executives - "earnings", # earnings history - "earningsHistory", - "earningsTrend", # earnings trend - "industryTrend", - "indexTrend", - "sectorTrend", - "recommendationTrend", - "futuresChain", -) - -# map last updated as of 2024.09.18 -SECTOR_INDUSTY_MAPPING = { - 'basic-materials': {'specialty-chemicals', - 'gold', - 'building-materials', - 'copper', - 'steel', - 'agricultural-inputs', - 'chemicals', - 'other-industrial-metals-mining', - 'lumber-wood-production', - 'aluminum', - 'other-precious-metals-mining', - 'coking-coal', - 'paper-paper-products', - 'silver'}, - 'communication-services': {'internet-content-information', - 'telecom-services', - 'entertainment', - 'electronic-gaming-multimedia', - 'advertising-agencies', - 'broadcasting', - 'publishing'}, - 'consumer-cyclical': {'internet-retail', - 'auto-manufacturers', - 'restaurants', - 'home-improvement-retail', - 'travel-services', - 'specialty-retail', - 'apparel-retail', - 'residential-construction', - 'footwear-accessories', - 'packaging-containers', - 'lodging', - 'auto-parts', - 'auto-truck-dealerships', - 'gambling', - 'resorts-casinos', - 'leisure', - 'apparel-manufacturing', - 'personal-services', - 'furnishings-fixtures-appliances', - 'recreational-vehicles', - 'luxury-goods', - 'department-stores', - 'textile-manufacturing'}, - 'consumer-defensive': {'discount-stores', - 'beverages-non-alcoholic', - 'household-personal-products', - 'packaged-foods', - 'tobacco', - 'confectioners', - 'farm-products', - 'food-distribution', - 'grocery-stores', - 'beverages-brewers', - 'education-training-services', - 'beverages-wineries-distilleries'}, - 'energy': {'oil-gas-integrated', - 'oil-gas-midstream', - 'oil-gas-e-p', - 'oil-gas-equipment-services', - 'oil-gas-refining-marketing', - 'uranium', - 'oil-gas-drilling', - 'thermal-coal'}, - 'financial-services': {'banks-diversified', - 'credit-services', - 'asset-management', - 'insurance-diversified', - 'banks-regional', - 'capital-markets', - 'financial-data-stock-exchanges', - 'insurance-property-casualty', - 'insurance-brokers', - 'insurance-life', - 'insurance-specialty', - 'mortgage-finance', - 'insurance-reinsurance', - 'shell-companies', - 'financial-conglomerates'}, - 'healthcare': {'drug-manufacturers-general', - 'healthcare-plans', - 'biotechnology', - 'medical-devices', - 'diagnostics-research', - 'medical-instruments-supplies', - 'medical-care-facilities', - 'drug-manufacturers-specialty-generic', - 'health-information-services', - 'medical-distribution', - 'pharmaceutical-retailers'}, - 'industrials': {'aerospace-defense', - 'specialty-industrial-machinery', - 'railroads', - 'building-products-equipment', - 'farm-heavy-construction-machinery', - 'specialty-business-services', - 'integrated-freight-logistics', - 'waste-management', - 'conglomerates', - 'industrial-distribution', - 'engineering-construction', - 'rental-leasing-services', - 'consulting-services', - 'trucking', - 'electrical-equipment-parts', - 'airlines', - 'tools-accessories', - 'pollution-treatment-controls', - 'security-protection-services', - 'marine-shipping', - 'metal-fabrication', - 'infrastructure-operations', - 'staffing-employment-services', - 'airports-air-services', - 'business-equipment-supplies'}, - 'real-estate': {'reit-specialty', - 'reit-industrial', - 'reit-retail', - 'reit-residential', - 'reit-healthcare-facilities', - 'real-estate-services', - 'reit-office', - 'reit-diversified', - 'reit-mortgage', - 'reit-hotel-motel', - 'real-estate-development', - 'real-estate-diversified'}, - 'technology': {'software-infrastructure', - 'semiconductors', - 'consumer-electronics', - 'software-application', - 'information-technology-services', - 'semiconductor-equipment-materials', - 'communication-equipment', - 'computer-hardware', - 'electronic-components', - 'scientific-technical-instruments', - 'solar', - 'electronics-computer-distribution'}, - 'utilities': {'utilities-regulated-electric', - 'utilities-renewable', - 'utilities-diversified', - 'utilities-regulated-gas', - 'utilities-independent-power-producers', - 'utilities-regulated-water'} -} - -def merge_two_level_dicts(dict1, dict2): - result = dict1.copy() - for key, value in dict2.items(): - if key in result: - # If both are sets, merge them - if isinstance(value, set) and isinstance(result[key], set): - result[key] = result[key] | value - # If both are dicts, merge their contents - elif isinstance(value, dict) and isinstance(result[key], dict): - result[key] = { - k: (result[key].get(k, set()) | v if isinstance(v, set) - else v) if k in result[key] - else v - for k, v in value.items() - } - else: - result[key] = value - return result - -EQUITY_SCREENER_EQ_MAP = { - "exchange": { - 'ar': {'BUE'}, - 'at': {'VIE'}, - 'au': {'ASX'}, - 'be': {'BRU'}, - 'br': {'SAO'}, - 'ca': {'CNQ', 'NEO', 'TOR', 'VAN'}, - 'ch': {'EBS'}, - 'cl': {'SGO'}, - 'cn': {'SHH', 'SHZ'}, - 'co': {'BVC'}, - 'cz': {'PRA'}, - 'de': {'BER', 'DUS', 'FRA', 'HAM', 'GER', 'MUN', 'STU'}, - 'dk': {'CPH'}, - 'ee': {'TAL'}, - 'eg': {'CAI'}, - 'es': {'MCE'}, - 'fi': {'HEL'}, - 'fr': {'PAR'}, - 'gb': {'AQS', 'IOB', 'LSE'}, - 'gr': {'ATH'}, - 'hk': {'HKG'}, - 'hu': {'BUD'}, - 'id': {'JKT'}, - 'ie': {'ISE'}, - 'il': {'TLV'}, - 'in': {'BSE', 'NSI'}, - 'is': {'ICE'}, - 'it': {'MIL'}, - 'jp': {'FKA', 'JPX', 'SAP'}, - 'kr': {'KOE', 'KSC'}, - 'kw': {'KUW'}, - 'lk': {}, - 'lt': {'LIT'}, - 'lv': {'RIS'}, - 'mx': {'MEX'}, - 'my': {'KLS'}, - 'nl': {'AMS'}, - 'no': {'OSL'}, - 'nz': {'NZE'}, - 'pe': {}, - 'ph': {'PHP', 'PHS'}, - 'pk': {}, - 'pl': {'WSE'}, - 'pt': {'LIS'}, - 'qa': {'DOH'}, - 'ro': {'BVB'}, - 'ru': {}, - 'sa': {'SAU'}, - 'se': {'STO'}, - 'sg': {'SES'}, - 'sr': {}, - 'th': {'SET'}, - 'tr': {'IST'}, - 'tw': {'TAI', 'TWO'}, - 'us': {'ASE', 'BTS', 'CXI', 'NCM', 'NGM', 'NMS', 'NYQ', 'OEM', 'OQB', 'OQX', 'PCX', 'PNK', 'YHD'}, - 've': {'CCS'}, - 'vn': {}, - 'za': {'JNB'} - }, - "sector": { - "Basic Materials", "Industrials", "Communication Services", "Healthcare", - "Real Estate", "Technology", "Energy", "Utilities", "Financial Services", - "Consumer Defensive", "Consumer Cyclical" - }, - "peer_group": { - "US Fund Equity Energy", - "US CE Convertibles", - "EAA CE UK Large-Cap Equity", - "EAA CE Other", - "US Fund Financial", - "India CE Multi-Cap", - "US Fund Foreign Large Blend", - "US Fund Consumer Cyclical", - "EAA Fund Global Equity Income", - "China Fund Sector Equity Financial and Real Estate", - "US Fund Equity Precious Metals", - "EAA Fund RMB Bond - Onshore", - "China Fund QDII Greater China Equity", - "US Fund Large Growth", - "EAA Fund Germany Equity", - "EAA Fund Hong Kong Equity", - "EAA CE UK Small-Cap Equity", - "US Fund Natural Resources", - "US CE Preferred Stock", - "India Fund Sector - Financial Services", - "US Fund Diversified Emerging Mkts", - "EAA Fund South Africa & Namibia Equity", - "China Fund QDII Sector Equity", - "EAA CE Sector Equity Biotechnology", - "EAA Fund Switzerland Equity", - "US Fund Large Value", - "EAA Fund Asia ex-Japan Equity", - "US Fund Health", - "US Fund China Region", - "EAA Fund Emerging Europe ex-Russia Equity", - "EAA Fund Sector Equity Industrial Materials", - "EAA Fund Japan Large-Cap Equity", - "EAA Fund EUR Corporate Bond", - "US Fund Technology", - "EAA CE Global Large-Cap Blend Equity", - "Mexico Fund Mexico Equity", - "US Fund Trading--Leveraged Equity", - "EAA Fund Sector Equity Consumer Goods & Services", - "US Fund Large Blend", - "EAA Fund Global Flex-Cap Equity", - "EAA Fund EUR Aggressive Allocation - Global", - "EAA Fund China Equity", - "EAA Fund Global Large-Cap Growth Equity", - "US CE Options-based", - "EAA Fund Sector Equity Financial Services", - "EAA Fund Europe Large-Cap Blend Equity", - "EAA Fund China Equity - A Shares", - "EAA Fund USD Corporate Bond", - "EAA Fund Eurozone Large-Cap Equity", - "China Fund Aggressive Allocation Fund", - "EAA Fund Sector Equity Technology", - "EAA Fund Global Emerging Markets Equity", - "EAA Fund EUR Moderate Allocation - Global", - "EAA Fund Other Bond", - "EAA Fund Denmark Equity", - "EAA Fund US Large-Cap Blend Equity", - "India Fund Large-Cap", - "Paper & Forestry", - "Containers & Packaging", - "US Fund Miscellaneous Region", - "Energy Services", - "EAA Fund Other Equity", - "Homebuilders", - "Construction Materials", - "China Fund Equity Funds", - "Steel", - "Consumer Durables", - "EAA Fund Global Large-Cap Blend Equity", - "Transportation Infrastructure", - "Precious Metals", - "Building Products", - "Traders & Distributors", - "Electrical Equipment", - "Auto Components", - "Construction & Engineering", - "Aerospace & Defense", - "Refiners & Pipelines", - "Diversified Metals", - "Textiles & Apparel", - "Industrial Conglomerates", - "Household Products", - "Commercial Services", - "Food Retailers", - "Semiconductors", - "Media", - "Automobiles", - "Consumer Services", - "Technology Hardware", - "Transportation", - "Telecommunication Services", - "Oil & Gas Producers", - "Machinery", - "Retailing", - "Healthcare", - "Chemicals", - "Food Products", - "Diversified Financials", - "Real Estate", - "Insurance", - "Utilities", - "Pharmaceuticals", - "Software & Services", - "Banks" - } -} -EQUITY_SCREENER_EQ_MAP['region'] = EQUITY_SCREENER_EQ_MAP['exchange'].keys() -ordered_keys = ['region'] + [k for k in EQUITY_SCREENER_EQ_MAP.keys() if k != 'region'] -EQUITY_SCREENER_EQ_MAP = {k:EQUITY_SCREENER_EQ_MAP[k] for k in ordered_keys} -FUND_SCREENER_EQ_MAP = { - "exchange": { - 'us': {'NAS'} - } -} -COMMON_SCREENER_FIELDS = { - "price":{ - "eodprice", - "intradaypricechange", - "intradayprice" - }, - "eq_fields": { - "exchange"}, -} -FUND_SCREENER_FIELDS = { - "eq_fields": { - "categoryname", - "performanceratingoverall", - "initialinvestment", - "annualreturnnavy1categoryrank", - "riskratingoverall"} -} -FUND_SCREENER_FIELDS = merge_two_level_dicts(FUND_SCREENER_FIELDS, COMMON_SCREENER_FIELDS) -EQUITY_SCREENER_FIELDS = { - "eq_fields": { - "region", - "sector", - "peer_group"}, - "price":{ - "lastclosemarketcap.lasttwelvemonths", - "percentchange", - "lastclose52weekhigh.lasttwelvemonths", - "fiftytwowkpercentchange", - "lastclose52weeklow.lasttwelvemonths", - "intradaymarketcap"}, - "trading":{ - "beta", - "avgdailyvol3m", - "pctheldinsider", - "pctheldinst", - "dayvolume", - "eodvolume"}, - "short_interest":{ - "short_percentage_of_shares_outstanding.value", - "short_interest.value", - "short_percentage_of_float.value", - "days_to_cover_short.value", - "short_interest_percentage_change.value"}, - "valuation":{ - "bookvalueshare.lasttwelvemonths", - "lastclosemarketcaptotalrevenue.lasttwelvemonths", - "lastclosetevtotalrevenue.lasttwelvemonths", - "pricebookratio.quarterly", - "peratio.lasttwelvemonths", - "lastclosepricetangiblebookvalue.lasttwelvemonths", - "lastclosepriceearnings.lasttwelvemonths", - "pegratio_5y"}, - "profitability":{ - "consecutive_years_of_dividend_growth_count", - "returnonassets.lasttwelvemonths", - "returnonequity.lasttwelvemonths", - "forward_dividend_per_share", - "forward_dividend_yield", - "returnontotalcapital.lasttwelvemonths"}, - "leverage":{ - "lastclosetevebit.lasttwelvemonths", - "netdebtebitda.lasttwelvemonths", - "totaldebtequity.lasttwelvemonths", - "ltdebtequity.lasttwelvemonths", - "ebitinterestexpense.lasttwelvemonths", - "ebitdainterestexpense.lasttwelvemonths", - "lastclosetevebitda.lasttwelvemonths", - "totaldebtebitda.lasttwelvemonths"}, - "liquidity":{ - "quickratio.lasttwelvemonths", - "altmanzscoreusingtheaveragestockinformationforaperiod.lasttwelvemonths", - "currentratio.lasttwelvemonths", - "operatingcashflowtocurrentliabilities.lasttwelvemonths"}, - "income_statement":{ - "totalrevenues.lasttwelvemonths", - "netincomemargin.lasttwelvemonths", - "grossprofit.lasttwelvemonths", - "ebitda1yrgrowth.lasttwelvemonths", - "dilutedepscontinuingoperations.lasttwelvemonths", - "quarterlyrevenuegrowth.quarterly", - "epsgrowth.lasttwelvemonths", - "netincomeis.lasttwelvemonths", - "ebitda.lasttwelvemonths", - "dilutedeps1yrgrowth.lasttwelvemonths", - "totalrevenues1yrgrowth.lasttwelvemonths", - "operatingincome.lasttwelvemonths", - "netincome1yrgrowth.lasttwelvemonths", - "grossprofitmargin.lasttwelvemonths", - "ebitdamargin.lasttwelvemonths", - "ebit.lasttwelvemonths", - "basicepscontinuingoperations.lasttwelvemonths", - "netepsbasic.lasttwelvemonths" - "netepsdiluted.lasttwelvemonths"}, - "balance_sheet":{ - "totalassets.lasttwelvemonths", - "totalcommonsharesoutstanding.lasttwelvemonths", - "totaldebt.lasttwelvemonths", - "totalequity.lasttwelvemonths", - "totalcurrentassets.lasttwelvemonths", - "totalcashandshortterminvestments.lasttwelvemonths", - "totalcommonequity.lasttwelvemonths", - "totalcurrentliabilities.lasttwelvemonths", - "totalsharesoutstanding"}, - "cash_flow":{ - "forward_dividend_yield", - "leveredfreecashflow.lasttwelvemonths", - "capitalexpenditure.lasttwelvemonths", - "cashfromoperations.lasttwelvemonths", - "leveredfreecashflow1yrgrowth.lasttwelvemonths", - "unleveredfreecashflow.lasttwelvemonths", - "cashfromoperations1yrgrowth.lasttwelvemonths"}, - "esg":{ - "esg_score", - "environmental_score", - "governance_score", - "social_score", - "highest_controversy"} -} -EQUITY_SCREENER_FIELDS = merge_two_level_dicts(EQUITY_SCREENER_FIELDS, COMMON_SCREENER_FIELDS) +_PRICE_COLNAMES_ = ['Open', 'High', 'Low', 'Close', 'Adj Close'] \ No newline at end of file diff --git a/yfinance/screener/__init__.py b/yfinance/screener/__init__.py index b3e312242..97e008503 100644 --- a/yfinance/screener/__init__.py +++ b/yfinance/screener/__init__.py @@ -1,4 +1,4 @@ -from .query import EquityQuery -from .screener import screen, PREDEFINED_SCREENER_QUERIES +from .query import EquityQuery, screen, FundQuery, Query, QueryHead +from .const import PREDEFINED_SCREENER_QUERIES -__all__ = ['EquityQuery', 'FundQuery', 'screen', 'PREDEFINED_SCREENER_QUERIES'] +__all__ = ["EquityQuery", "FundQuery", "screen", "PREDEFINED_SCREENER_QUERIES", "Query", "QueryHead"] diff --git a/yfinance/screener/const.py b/yfinance/screener/const.py new file mode 100644 index 000000000..58cfe1c41 --- /dev/null +++ b/yfinance/screener/const.py @@ -0,0 +1,494 @@ +from yfinance.const import _BASE_URL_ +from yfinance.utils import merge_two_level_dicts + +quote_summary_valid_modules = ( + "summaryProfile", # contains general information about the company + "summaryDetail", # prices + volume + market cap + etc + "assetProfile", # summaryProfile + company officers + "fundProfile", + "price", # current prices + "quoteType", # quoteType + "esgScores", # Environmental, social, and governance (ESG) scores, sustainability and ethical performance of companies + "incomeStatementHistory", + "incomeStatementHistoryQuarterly", + "balanceSheetHistory", + "balanceSheetHistoryQuarterly", + "cashFlowStatementHistory", + "cashFlowStatementHistoryQuarterly", + "defaultKeyStatistics", # KPIs (PE, enterprise value, EPS, EBITA, and more) + "financialData", # Financial KPIs (revenue, gross margins, operating cash flow, free cash flow, and more) + "calendarEvents", # future earnings date + "secFilings", # SEC filings, such as 10K and 10Q reports + "upgradeDowngradeHistory", # upgrades and downgrades that analysts have given a company's stock + "institutionOwnership", # institutional ownership, holders and shares outstanding + "fundOwnership", # mutual fund ownership, holders and shares outstanding + "majorDirectHolders", + "majorHoldersBreakdown", + "insiderTransactions", # insider transactions, such as the number of shares bought and sold by company executives + "insiderHolders", # insider holders, such as the number of shares held by company executives + "netSharePurchaseActivity", # net share purchase activity, such as the number of shares bought and sold by company executives + "earnings", # earnings history + "earningsHistory", + "earningsTrend", # earnings trend + "industryTrend", + "indexTrend", + "sectorTrend", + "recommendationTrend", + "futuresChain", +) + +# map last updated as of 2024.09.18 +SECTOR_INDUSTY_MAPPING = { + 'basic-materials': {'specialty-chemicals', + 'gold', + 'building-materials', + 'copper', + 'steel', + 'agricultural-inputs', + 'chemicals', + 'other-industrial-metals-mining', + 'lumber-wood-production', + 'aluminum', + 'other-precious-metals-mining', + 'coking-coal', + 'paper-paper-products', + 'silver'}, + 'communication-services': {'internet-content-information', + 'telecom-services', + 'entertainment', + 'electronic-gaming-multimedia', + 'advertising-agencies', + 'broadcasting', + 'publishing'}, + 'consumer-cyclical': {'internet-retail', + 'auto-manufacturers', + 'restaurants', + 'home-improvement-retail', + 'travel-services', + 'specialty-retail', + 'apparel-retail', + 'residential-construction', + 'footwear-accessories', + 'packaging-containers', + 'lodging', + 'auto-parts', + 'auto-truck-dealerships', + 'gambling', + 'resorts-casinos', + 'leisure', + 'apparel-manufacturing', + 'personal-services', + 'furnishings-fixtures-appliances', + 'recreational-vehicles', + 'luxury-goods', + 'department-stores', + 'textile-manufacturing'}, + 'consumer-defensive': {'discount-stores', + 'beverages-non-alcoholic', + 'household-personal-products', + 'packaged-foods', + 'tobacco', + 'confectioners', + 'farm-products', + 'food-distribution', + 'grocery-stores', + 'beverages-brewers', + 'education-training-services', + 'beverages-wineries-distilleries'}, + 'energy': {'oil-gas-integrated', + 'oil-gas-midstream', + 'oil-gas-e-p', + 'oil-gas-equipment-services', + 'oil-gas-refining-marketing', + 'uranium', + 'oil-gas-drilling', + 'thermal-coal'}, + 'financial-services': {'banks-diversified', + 'credit-services', + 'asset-management', + 'insurance-diversified', + 'banks-regional', + 'capital-markets', + 'financial-data-stock-exchanges', + 'insurance-property-casualty', + 'insurance-brokers', + 'insurance-life', + 'insurance-specialty', + 'mortgage-finance', + 'insurance-reinsurance', + 'shell-companies', + 'financial-conglomerates'}, + 'healthcare': {'drug-manufacturers-general', + 'healthcare-plans', + 'biotechnology', + 'medical-devices', + 'diagnostics-research', + 'medical-instruments-supplies', + 'medical-care-facilities', + 'drug-manufacturers-specialty-generic', + 'health-information-services', + 'medical-distribution', + 'pharmaceutical-retailers'}, + 'industrials': {'aerospace-defense', + 'specialty-industrial-machinery', + 'railroads', + 'building-products-equipment', + 'farm-heavy-construction-machinery', + 'specialty-business-services', + 'integrated-freight-logistics', + 'waste-management', + 'conglomerates', + 'industrial-distribution', + 'engineering-construction', + 'rental-leasing-services', + 'consulting-services', + 'trucking', + 'electrical-equipment-parts', + 'airlines', + 'tools-accessories', + 'pollution-treatment-controls', + 'security-protection-services', + 'marine-shipping', + 'metal-fabrication', + 'infrastructure-operations', + 'staffing-employment-services', + 'airports-air-services', + 'business-equipment-supplies'}, + 'real-estate': {'reit-specialty', + 'reit-industrial', + 'reit-retail', + 'reit-residential', + 'reit-healthcare-facilities', + 'real-estate-services', + 'reit-office', + 'reit-diversified', + 'reit-mortgage', + 'reit-hotel-motel', + 'real-estate-development', + 'real-estate-diversified'}, + 'technology': {'software-infrastructure', + 'semiconductors', + 'consumer-electronics', + 'software-application', + 'information-technology-services', + 'semiconductor-equipment-materials', + 'communication-equipment', + 'computer-hardware', + 'electronic-components', + 'scientific-technical-instruments', + 'solar', + 'electronics-computer-distribution'}, + 'utilities': {'utilities-regulated-electric', + 'utilities-renewable', + 'utilities-diversified', + 'utilities-regulated-gas', + 'utilities-independent-power-producers', + 'utilities-regulated-water'} +} + +EQUITY_SCREENER_EQ_MAP = { + "exchange": { + 'ar': {'BUE'}, + 'at': {'VIE'}, + 'au': {'ASX'}, + 'be': {'BRU'}, + 'br': {'SAO'}, + 'ca': {'CNQ', 'NEO', 'TOR', 'VAN'}, + 'ch': {'EBS'}, + 'cl': {'SGO'}, + 'cn': {'SHH', 'SHZ'}, + 'co': {'BVC'}, + 'cz': {'PRA'}, + 'de': {'BER', 'DUS', 'FRA', 'HAM', 'GER', 'MUN', 'STU'}, + 'dk': {'CPH'}, + 'ee': {'TAL'}, + 'eg': {'CAI'}, + 'es': {'MCE'}, + 'fi': {'HEL'}, + 'fr': {'PAR'}, + 'gb': {'AQS', 'IOB', 'LSE'}, + 'gr': {'ATH'}, + 'hk': {'HKG'}, + 'hu': {'BUD'}, + 'id': {'JKT'}, + 'ie': {'ISE'}, + 'il': {'TLV'}, + 'in': {'BSE', 'NSI'}, + 'is': {'ICE'}, + 'it': {'MIL'}, + 'jp': {'FKA', 'JPX', 'SAP'}, + 'kr': {'KOE', 'KSC'}, + 'kw': {'KUW'}, + 'lk': {}, + 'lt': {'LIT'}, + 'lv': {'RIS'}, + 'mx': {'MEX'}, + 'my': {'KLS'}, + 'nl': {'AMS'}, + 'no': {'OSL'}, + 'nz': {'NZE'}, + 'pe': {}, + 'ph': {'PHP', 'PHS'}, + 'pk': {}, + 'pl': {'WSE'}, + 'pt': {'LIS'}, + 'qa': {'DOH'}, + 'ro': {'BVB'}, + 'ru': {}, + 'sa': {'SAU'}, + 'se': {'STO'}, + 'sg': {'SES'}, + 'sr': {}, + 'th': {'SET'}, + 'tr': {'IST'}, + 'tw': {'TAI', 'TWO'}, + 'us': {'ASE', 'BTS', 'CXI', 'NCM', 'NGM', 'NMS', 'NYQ', 'OEM', 'OQB', 'OQX', 'PCX', 'PNK', 'YHD'}, + 've': {'CCS'}, + 'vn': {}, + 'za': {'JNB'} + }, + "sector": { + "Basic Materials", "Industrials", "Communication Services", "Healthcare", + "Real Estate", "Technology", "Energy", "Utilities", "Financial Services", + "Consumer Defensive", "Consumer Cyclical" + }, + "peer_group": { + "US Fund Equity Energy", + "US CE Convertibles", + "EAA CE UK Large-Cap Equity", + "EAA CE Other", + "US Fund Financial", + "India CE Multi-Cap", + "US Fund Foreign Large Blend", + "US Fund Consumer Cyclical", + "EAA Fund Global Equity Income", + "China Fund Sector Equity Financial and Real Estate", + "US Fund Equity Precious Metals", + "EAA Fund RMB Bond - Onshore", + "China Fund QDII Greater China Equity", + "US Fund Large Growth", + "EAA Fund Germany Equity", + "EAA Fund Hong Kong Equity", + "EAA CE UK Small-Cap Equity", + "US Fund Natural Resources", + "US CE Preferred Stock", + "India Fund Sector - Financial Services", + "US Fund Diversified Emerging Mkts", + "EAA Fund South Africa & Namibia Equity", + "China Fund QDII Sector Equity", + "EAA CE Sector Equity Biotechnology", + "EAA Fund Switzerland Equity", + "US Fund Large Value", + "EAA Fund Asia ex-Japan Equity", + "US Fund Health", + "US Fund China Region", + "EAA Fund Emerging Europe ex-Russia Equity", + "EAA Fund Sector Equity Industrial Materials", + "EAA Fund Japan Large-Cap Equity", + "EAA Fund EUR Corporate Bond", + "US Fund Technology", + "EAA CE Global Large-Cap Blend Equity", + "Mexico Fund Mexico Equity", + "US Fund Trading--Leveraged Equity", + "EAA Fund Sector Equity Consumer Goods & Services", + "US Fund Large Blend", + "EAA Fund Global Flex-Cap Equity", + "EAA Fund EUR Aggressive Allocation - Global", + "EAA Fund China Equity", + "EAA Fund Global Large-Cap Growth Equity", + "US CE Options-based", + "EAA Fund Sector Equity Financial Services", + "EAA Fund Europe Large-Cap Blend Equity", + "EAA Fund China Equity - A Shares", + "EAA Fund USD Corporate Bond", + "EAA Fund Eurozone Large-Cap Equity", + "China Fund Aggressive Allocation Fund", + "EAA Fund Sector Equity Technology", + "EAA Fund Global Emerging Markets Equity", + "EAA Fund EUR Moderate Allocation - Global", + "EAA Fund Other Bond", + "EAA Fund Denmark Equity", + "EAA Fund US Large-Cap Blend Equity", + "India Fund Large-Cap", + "Paper & Forestry", + "Containers & Packaging", + "US Fund Miscellaneous Region", + "Energy Services", + "EAA Fund Other Equity", + "Homebuilders", + "Construction Materials", + "China Fund Equity Funds", + "Steel", + "Consumer Durables", + "EAA Fund Global Large-Cap Blend Equity", + "Transportation Infrastructure", + "Precious Metals", + "Building Products", + "Traders & Distributors", + "Electrical Equipment", + "Auto Components", + "Construction & Engineering", + "Aerospace & Defense", + "Refiners & Pipelines", + "Diversified Metals", + "Textiles & Apparel", + "Industrial Conglomerates", + "Household Products", + "Commercial Services", + "Food Retailers", + "Semiconductors", + "Media", + "Automobiles", + "Consumer Services", + "Technology Hardware", + "Transportation", + "Telecommunication Services", + "Oil & Gas Producers", + "Machinery", + "Retailing", + "Healthcare", + "Chemicals", + "Food Products", + "Diversified Financials", + "Real Estate", + "Insurance", + "Utilities", + "Pharmaceuticals", + "Software & Services", + "Banks" + } +} +EQUITY_SCREENER_EQ_MAP['region'] = EQUITY_SCREENER_EQ_MAP['exchange'].keys() +ordered_keys = ['region'] + [k for k in EQUITY_SCREENER_EQ_MAP.keys() if k != 'region'] +EQUITY_SCREENER_EQ_MAP = {k:EQUITY_SCREENER_EQ_MAP[k] for k in ordered_keys} +FUND_SCREENER_EQ_MAP = { + "exchange": { + 'us': {'NAS'} + } +} +COMMON_SCREENER_FIELDS = { + "price":{ + "eodprice", + "intradaypricechange", + "intradayprice" + }, + "eq_fields": { + "exchange"}, +} +FUND_SCREENER_FIELDS = { + "eq_fields": { + "categoryname", + "performanceratingoverall", + "initialinvestment", + "annualreturnnavy1categoryrank", + "riskratingoverall"} +} +FUND_SCREENER_FIELDS = merge_two_level_dicts(FUND_SCREENER_FIELDS, COMMON_SCREENER_FIELDS) +EQUITY_SCREENER_FIELDS = { + "eq_fields": { + "region", + "sector", + "peer_group"}, + "price":{ + "lastclosemarketcap.lasttwelvemonths", + "percentchange", + "lastclose52weekhigh.lasttwelvemonths", + "fiftytwowkpercentchange", + "lastclose52weeklow.lasttwelvemonths", + "intradaymarketcap"}, + "trading":{ + "beta", + "avgdailyvol3m", + "pctheldinsider", + "pctheldinst", + "dayvolume", + "eodvolume"}, + "short_interest":{ + "short_percentage_of_shares_outstanding.value", + "short_interest.value", + "short_percentage_of_float.value", + "days_to_cover_short.value", + "short_interest_percentage_change.value"}, + "valuation":{ + "bookvalueshare.lasttwelvemonths", + "lastclosemarketcaptotalrevenue.lasttwelvemonths", + "lastclosetevtotalrevenue.lasttwelvemonths", + "pricebookratio.quarterly", + "peratio.lasttwelvemonths", + "lastclosepricetangiblebookvalue.lasttwelvemonths", + "lastclosepriceearnings.lasttwelvemonths", + "pegratio_5y"}, + "profitability":{ + "consecutive_years_of_dividend_growth_count", + "returnonassets.lasttwelvemonths", + "returnonequity.lasttwelvemonths", + "forward_dividend_per_share", + "forward_dividend_yield", + "returnontotalcapital.lasttwelvemonths"}, + "leverage":{ + "lastclosetevebit.lasttwelvemonths", + "netdebtebitda.lasttwelvemonths", + "totaldebtequity.lasttwelvemonths", + "ltdebtequity.lasttwelvemonths", + "ebitinterestexpense.lasttwelvemonths", + "ebitdainterestexpense.lasttwelvemonths", + "lastclosetevebitda.lasttwelvemonths", + "totaldebtebitda.lasttwelvemonths"}, + "liquidity":{ + "quickratio.lasttwelvemonths", + "altmanzscoreusingtheaveragestockinformationforaperiod.lasttwelvemonths", + "currentratio.lasttwelvemonths", + "operatingcashflowtocurrentliabilities.lasttwelvemonths"}, + "income_statement":{ + "totalrevenues.lasttwelvemonths", + "netincomemargin.lasttwelvemonths", + "grossprofit.lasttwelvemonths", + "ebitda1yrgrowth.lasttwelvemonths", + "dilutedepscontinuingoperations.lasttwelvemonths", + "quarterlyrevenuegrowth.quarterly", + "epsgrowth.lasttwelvemonths", + "netincomeis.lasttwelvemonths", + "ebitda.lasttwelvemonths", + "dilutedeps1yrgrowth.lasttwelvemonths", + "totalrevenues1yrgrowth.lasttwelvemonths", + "operatingincome.lasttwelvemonths", + "netincome1yrgrowth.lasttwelvemonths", + "grossprofitmargin.lasttwelvemonths", + "ebitdamargin.lasttwelvemonths", + "ebit.lasttwelvemonths", + "basicepscontinuingoperations.lasttwelvemonths", + "netepsbasic.lasttwelvemonths" + "netepsdiluted.lasttwelvemonths"}, + "balance_sheet":{ + "totalassets.lasttwelvemonths", + "totalcommonsharesoutstanding.lasttwelvemonths", + "totaldebt.lasttwelvemonths", + "totalequity.lasttwelvemonths", + "totalcurrentassets.lasttwelvemonths", + "totalcashandshortterminvestments.lasttwelvemonths", + "totalcommonequity.lasttwelvemonths", + "totalcurrentliabilities.lasttwelvemonths", + "totalsharesoutstanding"}, + "cash_flow":{ + "forward_dividend_yield", + "leveredfreecashflow.lasttwelvemonths", + "capitalexpenditure.lasttwelvemonths", + "cashfromoperations.lasttwelvemonths", + "leveredfreecashflow1yrgrowth.lasttwelvemonths", + "unleveredfreecashflow.lasttwelvemonths", + "cashfromoperations1yrgrowth.lasttwelvemonths"}, + "esg":{ + "esg_score", + "environmental_score", + "governance_score", + "social_score", + "highest_controversy"} +} +EQUITY_SCREENER_FIELDS = merge_two_level_dicts(EQUITY_SCREENER_FIELDS, COMMON_SCREENER_FIELDS) + +_SCREENER_URL_ = f"{_BASE_URL_}/v1/finance/screener" +_PREDEFINED_URL_ = f"{_SCREENER_URL_}/predefined/saved" + +PREDEFINED_SCREENER_BODY_DEFAULTS = { + "offset":0, "size":25, "userId":"","userIdType":"guid" +} diff --git a/yfinance/screener/query.py b/yfinance/screener/query.py index 81214a7d1..c17773749 100644 --- a/yfinance/screener/query.py +++ b/yfinance/screener/query.py @@ -1,16 +1,216 @@ from abc import ABC, abstractmethod import numbers -from typing import List, Union, Dict, TypeVar, Tuple +from typing import Union, TypeVar, Generic, Literal, TypedDict -from yfinance.const import EQUITY_SCREENER_EQ_MAP, EQUITY_SCREENER_FIELDS -from yfinance.const import FUND_SCREENER_EQ_MAP, FUND_SCREENER_FIELDS +from .const import EQUITY_SCREENER_EQ_MAP, EQUITY_SCREENER_FIELDS, FUND_SCREENER_EQ_MAP, FUND_SCREENER_FIELDS, _PREDEFINED_URL_, _SCREENER_URL_ +from yfinance.data import YfData from yfinance.exceptions import YFNotImplementedError from ..utils import dynamic_docstring, generate_list_table_from_dict_universal +import requests +from typing import Union, TypeVar, Generic, Literal, TypedDict -T = TypeVar('T', bound=Union[str, numbers.Real]) +class RESULT(TypedDict, Generic['OPERATOR', 'OPERAND']): + operator: 'OPERATOR' + operands: 'OPERAND' -class QueryBase(ABC): - def __init__(self, operator: str, operand: Union[ List['QueryBase'], Tuple[str, Tuple[Union[str, numbers.Real], ...]] ]): +OPERATORS = Literal["EQ", "OR", "AND", "BTWN", "GT", "LT", "GTE", "LTE"] +OPERANDS = Union[list['QueryBase'],list[str],list[Union[str, float, int]],list[RESULT['OPERATOR', 'OPERANDS']]] + +OPERATOR = TypeVar("OPERATOR", bound=OPERATORS) +OPERAND = TypeVar("OPERAND", bound=OPERANDS) + + +ISIN = 'QueryBase["OR", list[QueryBase["EQ", OPERANDS]]]' + + +class Query: + """ + - `Query.screen` is equivalent to `yfinance.screen(query=query)` but it uses the settings of the `Query` object. + - `Query.to_dict` returns a dictionary representation of the `Query` object. + - `Query.head` returns the `QueryHead` object. + - `Query.exchange` returns the exchange of the `Query` object from the `QueryHead` object. + - `Query.region` returns the region of the `Query` object from the `QueryHead` object. + - `Query.sector` returns the sector of the `Query` object from the `QueryHead` object. + """ + def __init__( + self, + offset: 'int' = None, + size: 'int' = None, + sortField: 'str' = None, + sortAsc: 'bool' = None, + userId: 'str' = None, + userIdType: 'str' = None, + ): + """ + Args: + offset (int, optional): The offset of the first record to return. Defaults to None. + size (int, optional): The number of records to return. Defaults to None. + sortField (str, optional): The field to sort by. Defaults to None. + sortAsc (bool, optional): Whether to sort ascending or descending. Defaults to None. + userId (str, optional): The user ID to filter by. Defaults to None. + userIdType (str, optional): The type of user ID. Defaults to None. + + Defaults: + offset = 0 + size = 25 + sortField = "ticker" + sortAsc = False + userId = "" + userIdType = "guid" + """ + self._query = None + self._head = None + self.offset = offset or 0 + self.size = size or 25 + self.sortField = sortField or "ticker" + self.sortAsc = sortAsc or False + self.userId = userId or "" + self.userIdType = userIdType or "guid" + + @property + def query(self) -> 'Union[QueryBase, None]': + return self._query + + @query.setter + def query(self, value: 'Union[QueryBase, None]'): + self._query = value + + @property + def head(self) -> 'Union[QueryHead, None]': + return self._head + + @head.setter + def head(self, value: 'Union[QueryHead, None]'): + self._head = value + + @property + def exchange(self) -> 'Union[str, QueryBase, None]': + if self.head is not None: + return self.head.exchange + return None + + @exchange.setter + def exchange(self, value: 'Union[str, QueryBase, None]'): + if self.head is None: + self.head = QueryHead() + self.head.exchange = value + + @property + def region(self) -> 'Union[str, QueryBase, None]': + if self.head is not None: + return self.head.region + return None + + @region.setter + def region(self, value: 'Union[str, QueryBase, None]'): + if self.head is None: + self.head = QueryHead() + self.head.region = value + + @property + def sector(self) -> 'Union[str, QueryBase, None]': + if self.head is not None: + return self.head.sector + return None + + @sector.setter + def sector(self, value: 'Union[str, QueryBase, None]'): + if self.head is None: + self.head = QueryHead() + self.head.sector = value + + @property + def fields(self) -> 'dict': + return { + "offset": self.offset, + "size": self.size, + "sortField": self.sortField, + "sortType": "ASC" if self.sortAsc else "DESC", + "userId": self.userId, + "userIdType": self.userIdType, + "quoteType": "EQUITY" if isinstance(self.query, EquityQuery) else "MUTUALFUND" + } + + def to_dict(self): + operands = [] + if self.query is not None: + operands.append(self.query.to_dict()) + if self.head is not None: + operands.append(self.head.to_dict()) + return { + "operator": "AND", + "operands": operands + } + + def screen(self, session=None, proxy=None): + return screen( + self, + offset=self.offset, + size=self.size, + sortField=self.sortField, + sortAsc=self.sortAsc, + userId=self.userId, + userIdType=self.userIdType, + session=session, + proxy=proxy + ) + +class QueryHead: + region = None + sector = None + exchange = None + + def __init__(self, exchange: 'Union[str, QueryBase, None]' = None, region: 'Union[str, QueryBase, None]' = None, sector: 'Union[str, QueryBase, None]' = None): + self.exchange = exchange + self.region = region + self.sector = sector + + def to_dict(self) -> 'RESULT[Literal["AND"], OPERANDS]': + ret = { + "operator": "AND", + "operands": [] + } + if self.exchange is not None: + if isinstance(self.exchange, QueryBase): + ret["operands"].append({ + "operator": "EQ", + "operands": ["exchange", self.exchange.to_dict()] + }) + else: + ret["operands"].append({ + "operator": "EQ", + "operands": ["exchange", self.exchange] + }) + if self.region is not None: + if isinstance(self.region, QueryBase): + ret["operands"].append({ + "operator": "EQ", + "operands": ["region", self.region.to_dict()] + }) + else: + ret["operands"].append({ + "operator": "EQ", + "operands": ["region", self.region] + }) + if self.sector is not None: + if isinstance(self.sector, QueryBase): + ret["operands"].append({ + "operator": "EQ", + "operands": ["sector", self.sector.to_dict()] + }) + else: + ret["operands"].append({ + "operator": "EQ", + "operands": ["sector", self.sector] + }) + return ret + + + + + +class QueryBase(ABC, Generic[OPERATOR, OPERAND]): + def __init__(self, operator: 'OPERATOR', operand: 'OPERAND'): operator = operator.upper() if not isinstance(operand, list): @@ -18,9 +218,7 @@ def __init__(self, operator: str, operand: Union[ List['QueryBase'], Tuple[str, if len(operand) <= 0: raise ValueError('Invalid field for EquityQuery') - if operator == 'IS-IN': - self._validate_isin_operand(operand) - elif operator in {'OR','AND'}: + if operator in {'OR','AND'}: self._validate_or_and_operand(operand) elif operator == 'EQ': self._validate_eq_operand(operand) @@ -34,23 +232,33 @@ def __init__(self, operator: str, operand: Union[ List['QueryBase'], Tuple[str, self.operator = operator self.operands = operand + @classmethod + def is_in(cls, value:'str', is_in:'list[str]') -> 'QueryBase[Literal["OR"], list[QueryBase[Literal["EQ"], OPERAND]]]': + # No validation needed as `OR` and `EQ` validation happens anyway + return type(cls)( + operator="OR", + operand=[ + type(cls)(operator="EQ", operand=[value, v]) for v in is_in + ] + ) + @property @abstractmethod - def valid_fields(self) -> List: + def valid_fields(self) -> 'list': raise YFNotImplementedError('valid_fields() needs to be implemented by child') @property @abstractmethod - def valid_values(self) -> Dict: + def valid_values(self) -> 'dict': raise YFNotImplementedError('valid_values() needs to be implemented by child') - def _validate_or_and_operand(self, operand: List['QueryBase']) -> None: + def _validate_or_and_operand(self, operand: 'list[QueryBase]') -> 'None': if len(operand) <= 1: raise ValueError('Operand must be length longer than 1') if all(isinstance(e, QueryBase) for e in operand) is False: raise TypeError(f'Operand must be type {type(self)} for OR/AND') - def _validate_eq_operand(self, operand: List[Union[str, numbers.Real]]) -> None: + def _validate_eq_operand(self, operand: 'list[Union[str, numbers.Real]]') -> 'None': if len(operand) != 2: raise ValueError('Operand must be length 2 for EQ') @@ -65,7 +273,7 @@ def _validate_eq_operand(self, operand: List[Union[str, numbers.Real]]) -> None: if operand[1] not in vv: raise ValueError(f'Invalid EQ value "{operand[1]}"') - def _validate_btwn_operand(self, operand: List[Union[str, numbers.Real]]) -> None: + def _validate_btwn_operand(self, operand: 'list[Union[str, numbers.Real]]') -> 'None': if len(operand) != 3: raise ValueError('Operand must be length 3 for BTWN') if not any(operand[0] in fields_by_type for fields_by_type in self.valid_fields.values()): @@ -75,7 +283,7 @@ def _validate_btwn_operand(self, operand: List[Union[str, numbers.Real]]) -> Non if isinstance(operand[2], numbers.Real) is False: raise TypeError('Invalid comparison type for BTWN') - def _validate_gt_lt(self, operand: List[Union[str, numbers.Real]]) -> None: + def _validate_gt_lt(self, operand: 'list[Union[str, numbers.Real]]') -> 'None': if len(operand) != 2: raise ValueError('Operand must be length 2 for GT/LT') if not any(operand[0] in fields_by_type for fields_by_type in self.valid_fields.values()): @@ -83,35 +291,15 @@ def _validate_gt_lt(self, operand: List[Union[str, numbers.Real]]) -> None: if isinstance(operand[1], numbers.Real) is False: raise TypeError('Invalid comparison type for GT/LT') - def _validate_isin_operand(self, operand: List['QueryBase']) -> None: - if len(operand) < 2: - raise ValueError('Operand must be length 2+ for IS-IN') - - if not any(operand[0] in fields_by_type for fields_by_type in self.valid_fields.values()): - raise ValueError(f'Invalid field for {type(self)} "{operand[0]}"') - if operand[0] in self.valid_values: - vv = self.valid_values[operand[0]] - if isinstance(vv, dict): - # this data structure is slightly different to generate better docs, - # need to unpack here. - vv = set().union(*[e for e in vv.values()]) - for i in range(1, len(operand)): - if operand[i] not in vv: - raise ValueError(f'Invalid EQ value "{operand[i]}"') - - def to_dict(self) -> Dict: + def to_dict(self) -> 'RESULT[OPERATOR, OPERAND]': op = self.operator ops = self.operands - if self.operator == 'IS-IN': - # Expand to OR of EQ queries - op = 'OR' - ops = [type(self)('EQ', [self.operands[0], v]) for v in self.operands[1:]] return { "operator": op, - "operands": [o.to_dict() if isinstance(o, QueryBase) else o for o in ops] + "operands": [o.to_dict() if isinstance(o, (QueryBase, QueryHead)) else o for o in ops] } - def __repr__(self, indent=0) -> str: + def __repr__(self, indent=0) -> 'str': indent_str = " " * indent class_name = self.__class__.__name__ @@ -131,7 +319,7 @@ def __repr__(self, indent=0) -> str: # Handle single operand return f"{class_name}({self.operator}, {repr(self.operands)})" - def __str__(self) -> str: + def __str__(self) -> 'str': return self.__repr__() @@ -139,7 +327,7 @@ class EquityQuery(QueryBase): """ The `EquityQuery` class constructs filters for stocks based on specific criteria such as region, sector, exchange, and peer group. - Start with value operations: `EQ` (equals), `IS-IN` (is in), `BTWN` (between), `GT` (greater than), `LT` (less than), `GTE` (greater or equal), `LTE` (less or equal). + Start with value operations: `EQ` (equals), `BTWN` (between), `GT` (greater than), `LT` (less than), `GTE` (greater or equal), `LTE` (less or equal). Combine them with logical operations: `AND`, `OR`. @@ -150,15 +338,15 @@ class EquityQuery(QueryBase): from yfinance import EquityQuery - EquityQuery('and', [ - EquityQuery('is-in', ['exchange', 'NMS', 'NYQ']), - EquityQuery('lt', ["epsgrowth.lasttwelvemonths", 15]) + EquityQuery("and", [ + EquityQuery.is_in("exchange", ["NMS", "NYQ"]), + EquityQuery("lt", ["epsgrowth.lasttwelvemonths", 15]) ]) """ @dynamic_docstring({"valid_operand_fields_table": generate_list_table_from_dict_universal(EQUITY_SCREENER_FIELDS)}) @property - def valid_fields(self) -> Dict: + def valid_fields(self) -> 'dict': """ Valid operands, grouped by category. {valid_operand_fields_table} @@ -167,7 +355,7 @@ def valid_fields(self) -> Dict: @dynamic_docstring({"valid_values_table": generate_list_table_from_dict_universal(EQUITY_SCREENER_EQ_MAP, concat_keys=['exchange'])}) @property - def valid_values(self) -> Dict: + def valid_values(self) -> 'dict': """ Most operands take number values, but some have a restricted set of valid values. {valid_values_table} @@ -179,7 +367,7 @@ class FundQuery(QueryBase): """ The `FundQuery` class constructs filters for mutual funds based on specific criteria such as region, sector, exchange, and peer group. - Start with value operations: `EQ` (equals), `IS-IN` (is in), `BTWN` (between), `GT` (greater than), `LT` (less than), `GTE` (greater or equal), `LTE` (less or equal). + Start with value operations: `EQ` (equals), `BTWN` (between), `GT` (greater than), `LT` (less than), `GTE` (greater or equal), `LTE` (less or equal). Combine them with logical operations: `AND`, `OR`. @@ -190,17 +378,17 @@ class FundQuery(QueryBase): from yfinance import FundQuery - FundQuery('and', [ - FundQuery('eq', ['categoryname', 'Large Growth']), - FundQuery('is-in', ['performanceratingoverall', 4, 5]), - FundQuery('lt', ['initialinvestment', 100001]), - FundQuery('lt', ['annualreturnnavy1categoryrank', 50]), - FundQuery('eq', ['exchange', 'NAS']) + FundQuery("and", [ + FundQuery("eq", ["categoryname", "Large Growth"]), + FundQuery.is_in("performanceratingoverall", [4, 5]), + FundQuery("lt", ["initialinvestment", 100001]), + FundQuery("lt", ["annualreturnnavy1categoryrank", 50]), + FundQuery("eq", ["exchange", "NAS"]) ]) """ @dynamic_docstring({"valid_operand_fields_table": generate_list_table_from_dict_universal(FUND_SCREENER_FIELDS)}) @property - def valid_fields(self) -> Dict: + def valid_fields(self) -> 'dict': """ Valid operands, grouped by category. {valid_operand_fields_table} @@ -209,10 +397,175 @@ def valid_fields(self) -> Dict: @dynamic_docstring({"valid_values_table": generate_list_table_from_dict_universal(FUND_SCREENER_EQ_MAP)}) @property - def valid_values(self) -> Dict: + def valid_values(self) -> 'dict': """ Most operands take number values, but some have a restricted set of valid values. {valid_values_table} """ return FUND_SCREENER_EQ_MAP + +@dynamic_docstring({"predefined_screeners": generate_list_table_from_dict_universal(PREDEFINED_SCREENER_QUERIES, bullets=True, title='Predefined queries (Dec-2024)')}) +def screen(query: 'Union[str, EquityQuery, FundQuery, Query]', + offset: 'int' = None, + size: 'int' = None, + sortField: 'str' = None, + sortAsc: 'bool' = None, + userId: 'str' = None, + userIdType: 'str' = None, + session = None, proxy = None): + """ + Run a screen: predefined query, or custom query. + + :Parameters: + * Defaults only apply if query = EquityQuery or FundQuery + query : str | Query: + The query to execute, either name of predefined or custom query. + For predefined list run yf.PREDEFINED_SCREENER_QUERIES.keys() + offset : int + The offset for the results. Default 0. + size : int + number of results to return. Default 100, maximum 250 (Yahoo) + sortField : str + field to sort by. Default "ticker" + sortAsc : bool + Sort ascending? Default False + userId : str + The user ID. Default empty. + userIdType : str + Type of user ID (e.g., "guid"). Default "guid". + + Example: predefined query + .. code-block:: python + + import yfinance as yf + response = yf.screen("aggressive_small_caps") + + Example: custom query + .. code-block:: python + + import yfinance as yf + from yfinance import EquityQuery + q = EquityQuery('and', [ + EquityQuery('gt', ['percentchange', 3]), + EquityQuery('eq', ['region', 'us']) + ]) + response = yf.screen(q, sortField = 'percentchange', sortAsc = True) + + To access predefineds query code + .. code-block:: python + + import yfinance as yf + query = yf.PREDEFINED_SCREENER_QUERIES['aggressive_small_caps'] + + {predefined_screeners} + """ + + # Only use defaults when user NOT give a predefined, because + # Yahoo's predefined endpoint auto-applies defaults. Also, + # that endpoint might be ignoring these fields. + defaults = { + 'offset': 0, + 'size': 25, + 'sortField': 'ticker', + 'sortAsc': False, + 'userId': "", + 'userIdType': "guid" + } + + if size is not None and size > 250: + raise ValueError("Yahoo limits query size to 250, reduce size.") + + fields = dict(locals()) + for k in ['query', 'session', 'proxy']: + if k in fields: + del fields[k] + + params_dict = {"corsDomain": "finance.yahoo.com", "formatted": "false", "lang": "en-US", "region": "US"} + + post_query = None + if isinstance(query, str): + # post_query = PREDEFINED_SCREENER_QUERIES[query] + # Switch to Yahoo's predefined endpoint + _data = YfData(session=session) + params_dict['scrIds'] = query + for k,v in fields.items(): + if v is not None: + params_dict[k] = v + resp = _data.get(url=_PREDEFINED_URL_, params=params_dict, proxy=proxy) + try: + resp.raise_for_status() + except requests.exceptions.HTTPError: + if query not in PREDEFINED_SCREENER_QUERIES: + print(f"yfinance.screen: '{query}' is probably not a predefined query.") + raise + return resp.json()["finance"]["result"][0] + + elif isinstance(query, QueryBase): + # Prepare other fields + for k in defaults: + if k not in fields or fields[k] is None: + fields[k] = defaults[k] + fields['sortType'] = 'ASC' if fields['sortAsc'] else 'DESC' + del fields['sortAsc'] + + post_query = fields + post_query['query'] = query.to_dict() + + if isinstance(post_query['query'], EqyQy): + post_query['quoteType'] = 'EQUITY' + elif isinstance(post_query['query'], FndQy): + post_query['quoteType'] = 'MUTUALFUND' + + elif isinstance(query, Query): + post_query = query.fields.copy() # Copy to avoid modifying original + post_query["query"] = query.to_dict() + + else: + raise ValueError(f'Query must be type str or QueryBase, not "{type(query)}"') + + if query is None: + raise ValueError('No query provided') + + # Fetch + _data = YfData(session=session) + response = _data.post(_SCREENER_URL_, + body=post_query, + user_agent_headers=_data.user_agent_headers, + params=params_dict, + proxy=proxy) + response.raise_for_status() + return response.json()['finance']['result'][0] + +PREDEFINED_SCREENER_QUERIES = { + "aggressive_small_caps": {"sortField":"eodvolume", "sortType":"desc", + "query": EqyQy("and", [EqyQy.is_in("exchange", ["NMS", "NYQ"]), EqyQy("lt", ["epsgrowth.lasttwelvemonths", 15])])}, + "day_gainers": {"sortField":"percentchange", "sortType":"DESC", + "query": EqyQy("and", [EqyQy("gt", ["percentchange", 3]), EqyQy("eq", ["region", "us"]), EqyQy("gte", ["intradaymarketcap", 2000000000]), EqyQy("gte", ["intradayprice", 5]), EqyQy("gt", ["dayvolume", 15000])])}, + "day_losers": {"sortField":"percentchange", "sortType":"ASC", + "query": EqyQy("and", [EqyQy("lt", ["percentchange", -2.5]), EqyQy("eq", ["region", "us"]), EqyQy("gte", ["intradaymarketcap", 2000000000]), EqyQy("gte", ["intradayprice", 5]), EqyQy("gt", ["dayvolume", 20000])])}, + "growth_technology_stocks": {"sortField":"eodvolume", "sortType":"desc", + "query": EqyQy("and", [EqyQy("gte", ["quarterlyrevenuegrowth.quarterly", 25]), EqyQy("gte", ["epsgrowth.lasttwelvemonths", 25]), EqyQy("eq", ["sector", "Technology"]), EqyQy.is_in("exchange", ["NMS", "NYQ"])])}, + "most_actives": {"sortField":"dayvolume", "sortType":"DESC", + "query": EqyQy("and", [EqyQy("eq", ["region", "us"]), EqyQy("gte", ["intradaymarketcap", 2000000000]), EqyQy("gt", ["dayvolume", 5000000])])}, + "most_shorted_stocks": {"size":25, "offset":0, "sortField":"short_percentage_of_shares_outstanding.value", "sortType":"DESC", + "query": EqyQy("and", [EqyQy("eq", ["region", "us"]), EqyQy("gt", ["intradayprice", 1]), EqyQy("gt", ["avgdailyvol3m", 200000])])}, + "small_cap_gainers": {"sortField":"eodvolume", "sortType":"desc", + "query": EqyQy("and", [EqyQy("lt", ["intradaymarketcap",2000000000]), EqyQy.is_in("exchange", ["NMS", "NYQ"])])}, + "undervalued_growth_stocks": {"sortType":"DESC", "sortField":"eodvolume", + "query": EqyQy("and", [EqyQy("btwn", ["peratio.lasttwelvemonths", 0, 20]), EqyQy("lt", ["pegratio_5y", 1]), EqyQy("gte", ["epsgrowth.lasttwelvemonths", 25]), EqyQy.is_in("exchange", ["NMS", "NYQ"])])}, + "undervalued_large_caps": {"sortField":"eodvolume", "sortType":"desc", + "query": EqyQy("and", [EqyQy("btwn", ["peratio.lasttwelvemonths", 0, 20]), EqyQy("lt", ["pegratio_5y", 1]), EqyQy("btwn", ["intradaymarketcap", 10000000000, 100000000000]), EqyQy.is_in("exchange", ["NMS", "NYQ"])])}, + "conservative_foreign_funds": {"sortType":"DESC", "sortField":"fundnetassets", + "query": FndQy("and", [FndQy.is_in("categoryname", ["Foreign Large Value", "Foreign Large Blend", "Foreign Large Growth", "Foreign Small/Mid Growth", "Foreign Small/Mid Blend", "Foreign Small/Mid Value"]), FndQy.is_in("performanceratingoverall", [4, 5]), FndQy("lt", ["initialinvestment", 100001]), FndQy("lt", ["annualreturnnavy1categoryrank", 50]), FndQy.is_in("riskratingoverall", [1, 2, 3]), FndQy("eq", ["exchange", "NAS"])])}, + "high_yield_bond": {"sortType":"DESC", "sortField":"fundnetassets", + "query": FndQy("and", [FndQy.is_in("performanceratingoverall", [4, 5]), FndQy("lt", ["initialinvestment", 100001]), FndQy("lt", ["annualreturnnavy1categoryrank", 50]), FndQy.is_in("riskratingoverall", [1, 2, 3]), FndQy("eq", ["categoryname", "High Yield Bond"]), FndQy("eq", ["exchange", "NAS"])])}, + "portfolio_anchors": {"sortType":"DESC", "sortField":"fundnetassets", + "query": FndQy("and", [FndQy("eq", ["categoryname", "Large Blend"]), FndQy.is_in("performanceratingoverall", [4, 5]), FndQy("lt", ["initialinvestment", 100001]), FndQy("lt", ["annualreturnnavy1categoryrank", 50]), FndQy("eq", ["exchange", "NAS"])])}, + "solid_large_growth_funds": {"sortType":"DESC", "sortField":"fundnetassets", + "query": FndQy("and", [FndQy("eq", ["categoryname", "Large Growth"]), FndQy.is_in("performanceratingoverall", [4, 5]), FndQy("lt", ["initialinvestment", 100001]), FndQy("lt", ["annualreturnnavy1categoryrank", 50]), FndQy("eq", ["exchange", "NAS"])])}, + "solid_midcap_growth_funds": {"sortType":"DESC", "sortField":"fundnetassets", + "query": FndQy("and", [FndQy("eq", ["categoryname", "Mid-Cap Growth"]), FndQy.is_in("performanceratingoverall", [4, 5]), FndQy("lt", ["initialinvestment", 100001]), FndQy("lt", ["annualreturnnavy1categoryrank", 50]), FndQy("eq", ["exchange", "NAS"])])}, + "top_mutual_funds": {"sortType":"DESC", "sortField":"percentchange", + "query": FndQy("and", [FndQy("gt", ["intradayprice", 15]), FndQy.is_in("performanceratingoverall", [4, 5]), FndQy("gt", ["initialinvestment", 1000]), FndQy("eq", ["exchange", "NAS"])])} +} diff --git a/yfinance/screener/screener.py b/yfinance/screener/screener.py deleted file mode 100644 index 60d84c318..000000000 --- a/yfinance/screener/screener.py +++ /dev/null @@ -1,180 +0,0 @@ -from .query import EquityQuery as EqyQy -from .query import FundQuery as FndQy -from .query import QueryBase, EquityQuery, FundQuery - -from yfinance.const import _BASE_URL_ -from yfinance.data import YfData - -from ..utils import dynamic_docstring, generate_list_table_from_dict_universal - -from typing import Union -import requests - -_SCREENER_URL_ = f"{_BASE_URL_}/v1/finance/screener" -_PREDEFINED_URL_ = f"{_SCREENER_URL_}/predefined/saved" - -PREDEFINED_SCREENER_BODY_DEFAULTS = { - "offset":0, "size":25, "userId":"","userIdType":"guid" -} - -PREDEFINED_SCREENER_QUERIES = { - 'aggressive_small_caps': {"sortField":"eodvolume", "sortType":"desc", - "query": EqyQy('and', [EqyQy('is-in', ['exchange', 'NMS', 'NYQ']), EqyQy('lt', ["epsgrowth.lasttwelvemonths", 15])])}, - 'day_gainers': {"sortField":"percentchange", "sortType":"DESC", - "query": EqyQy('and', [EqyQy('gt', ['percentchange', 3]), EqyQy('eq', ['region', 'us']), EqyQy('gte', ['intradaymarketcap', 2000000000]), EqyQy('gte', ['intradayprice', 5]), EqyQy('gt', ['dayvolume', 15000])])}, - 'day_losers': {"sortField":"percentchange", "sortType":"ASC", - "query": EqyQy('and', [EqyQy('lt', ['percentchange', -2.5]), EqyQy('eq', ['region', 'us']), EqyQy('gte', ['intradaymarketcap', 2000000000]), EqyQy('gte', ['intradayprice', 5]), EqyQy('gt', ['dayvolume', 20000])])}, - 'growth_technology_stocks': {"sortField":"eodvolume", "sortType":"desc", - "query": EqyQy('and', [EqyQy('gte', ['quarterlyrevenuegrowth.quarterly', 25]), EqyQy('gte', ['epsgrowth.lasttwelvemonths', 25]), EqyQy('eq', ['sector', 'Technology']), EqyQy('is-in', ['exchange', 'NMS', 'NYQ'])])}, - 'most_actives': {"sortField":"dayvolume", "sortType":"DESC", - "query": EqyQy('and', [EqyQy('eq', ['region', 'us']), EqyQy('gte', ['intradaymarketcap', 2000000000]), EqyQy('gt', ['dayvolume', 5000000])])}, - 'most_shorted_stocks': {"size":25, "offset":0, "sortField":"short_percentage_of_shares_outstanding.value", "sortType":"DESC", - "query": EqyQy('and', [EqyQy('eq', ['region', 'us']), EqyQy('gt', ['intradayprice', 1]), EqyQy('gt', ['avgdailyvol3m', 200000])])}, - 'small_cap_gainers': {"sortField":"eodvolume", "sortType":"desc", - "query": EqyQy("and", [EqyQy("lt", ["intradaymarketcap",2000000000]), EqyQy("is-in", ["exchange", "NMS", "NYQ"])])}, - 'undervalued_growth_stocks': {"sortType":"DESC", "sortField":"eodvolume", - "query": EqyQy('and', [EqyQy('btwn', ['peratio.lasttwelvemonths', 0, 20]), EqyQy('lt', ['pegratio_5y', 1]), EqyQy('gte', ['epsgrowth.lasttwelvemonths', 25]), EqyQy('is-in', ['exchange', 'NMS', 'NYQ'])])}, - 'undervalued_large_caps': {"sortField":"eodvolume", "sortType":"desc", - "query": EqyQy('and', [EqyQy('btwn', ['peratio.lasttwelvemonths', 0, 20]), EqyQy('lt', ['pegratio_5y', 1]), EqyQy('btwn', ['intradaymarketcap', 10000000000, 100000000000]), EqyQy('is-in', ['exchange', 'NMS', 'NYQ'])])}, - 'conservative_foreign_funds': {"sortType":"DESC", "sortField":"fundnetassets", - "query": FndQy('and', [FndQy('is-in', ['categoryname', 'Foreign Large Value', 'Foreign Large Blend', 'Foreign Large Growth', 'Foreign Small/Mid Growth', 'Foreign Small/Mid Blend', 'Foreign Small/Mid Value']), FndQy('is-in', ['performanceratingoverall', 4, 5]), FndQy('lt', ['initialinvestment', 100001]), FndQy('lt', ['annualreturnnavy1categoryrank', 50]), FndQy('is-in', ['riskratingoverall', 1, 2, 3]), FndQy('eq', ['exchange', 'NAS'])])}, - 'high_yield_bond': {"sortType":"DESC", "sortField":"fundnetassets", - "query": FndQy('and', [FndQy('is-in', ['performanceratingoverall', 4, 5]), FndQy('lt', ['initialinvestment', 100001]), FndQy('lt', ['annualreturnnavy1categoryrank', 50]), FndQy('is-in', ['riskratingoverall', 1, 2, 3]), FndQy('eq', ['categoryname', 'High Yield Bond']), FndQy('eq', ['exchange', 'NAS'])])}, - 'portfolio_anchors': {"sortType":"DESC", "sortField":"fundnetassets", - "query": FndQy('and', [FndQy('eq', ['categoryname', 'Large Blend']), FndQy('is-in', ['performanceratingoverall', 4, 5]), FndQy('lt', ['initialinvestment', 100001]), FndQy('lt', ['annualreturnnavy1categoryrank', 50]), FndQy('eq', ['exchange', 'NAS'])])}, - 'solid_large_growth_funds': {"sortType":"DESC", "sortField":"fundnetassets", - "query": FndQy('and', [FndQy('eq', ['categoryname', 'Large Growth']), FndQy('is-in', ['performanceratingoverall', 4, 5]), FndQy('lt', ['initialinvestment', 100001]), FndQy('lt', ['annualreturnnavy1categoryrank', 50]), FndQy('eq', ['exchange', 'NAS'])])}, - 'solid_midcap_growth_funds': {"sortType":"DESC", "sortField":"fundnetassets", - "query": FndQy('and', [FndQy('eq', ['categoryname', 'Mid-Cap Growth']), FndQy('is-in', ['performanceratingoverall', 4, 5]), FndQy('lt', ['initialinvestment', 100001]), FndQy('lt', ['annualreturnnavy1categoryrank', 50]), FndQy('eq', ['exchange', 'NAS'])])}, - 'top_mutual_funds': {"sortType":"DESC", "sortField":"percentchange", - "query": FndQy('and', [FndQy('gt', ['intradayprice', 15]), FndQy('is-in', ['performanceratingoverall', 4, 5]), FndQy('gt', ['initialinvestment', 1000]), FndQy('eq', ['exchange', 'NAS'])])} -} - -@dynamic_docstring({"predefined_screeners": generate_list_table_from_dict_universal(PREDEFINED_SCREENER_QUERIES, bullets=True, title='Predefined queries (Dec-2024)')}) -def screen(query: Union[str, EquityQuery, FundQuery], - offset: int = None, - size: int = None, - sortField: str = None, - sortAsc: bool = None, - userId: str = None, - userIdType: str = None, - session = None, proxy = None): - """ - Run a screen: predefined query, or custom query. - - :Parameters: - * Defaults only apply if query = EquityQuery or FundQuery - query : str | Query: - The query to execute, either name of predefined or custom query. - For predefined list run yf.PREDEFINED_SCREENER_QUERIES.keys() - offset : int - The offset for the results. Default 0. - size : int - number of results to return. Default 100, maximum 250 (Yahoo) - sortField : str - field to sort by. Default "ticker" - sortAsc : bool - Sort ascending? Default False - userId : str - The user ID. Default empty. - userIdType : str - Type of user ID (e.g., "guid"). Default "guid". - - Example: predefined query - .. code-block:: python - - import yfinance as yf - response = yf.screen("aggressive_small_caps") - - Example: custom query - .. code-block:: python - - import yfinance as yf - from yfinance import EquityQuery - q = EquityQuery('and', [ - EquityQuery('gt', ['percentchange', 3]), - EquityQuery('eq', ['region', 'us']) - ]) - response = yf.screen(q, sortField = 'percentchange', sortAsc = True) - - To access predefineds query code - .. code-block:: python - - import yfinance as yf - query = yf.PREDEFINED_SCREENER_QUERIES['aggressive_small_caps'] - - {predefined_screeners} - """ - - # Only use defaults when user NOT give a predefined, because - # Yahoo's predefined endpoint auto-applies defaults. Also, - # that endpoint might be ignoring these fields. - defaults = { - 'offset': 0, - 'size': 25, - 'sortField': 'ticker', - 'sortAsc': False, - 'userId': "", - 'userIdType': "guid" - } - - if size is not None and size > 250: - raise ValueError("Yahoo limits query size to 250, reduce size.") - - fields = dict(locals()) - for k in ['query', 'session', 'proxy']: - if k in fields: - del fields[k] - - params_dict = {"corsDomain": "finance.yahoo.com", "formatted": "false", "lang": "en-US", "region": "US"} - - post_query = None - if isinstance(query, str): - # post_query = PREDEFINED_SCREENER_QUERIES[query] - # Switch to Yahoo's predefined endpoint - _data = YfData(session=session) - params_dict['scrIds'] = query - for k,v in fields.items(): - if v is not None: - params_dict[k] = v - resp = _data.get(url=_PREDEFINED_URL_, params=params_dict, proxy=proxy) - try: - resp.raise_for_status() - except requests.exceptions.HTTPError: - if query not in PREDEFINED_SCREENER_QUERIES: - print(f"yfinance.screen: '{query}' is probably not a predefined query.") - raise - return resp.json()["finance"]["result"][0] - - elif isinstance(query, QueryBase): - # Prepare other fields - for k in defaults: - if k not in fields or fields[k] is None: - fields[k] = defaults[k] - fields['sortType'] = 'ASC' if fields['sortAsc'] else 'DESC' - del fields['sortAsc'] - - post_query = fields - post_query['query'] = query - - else: - raise ValueError(f'Query must be type str or QueryBase, not "{type(query)}"') - - if query is None: - raise ValueError('No query provided') - - if isinstance(post_query['query'], EqyQy): - post_query['quoteType'] = 'EQUITY' - elif isinstance(post_query['query'], FndQy): - post_query['quoteType'] = 'MUTUALFUND' - post_query['query'] = post_query['query'].to_dict() - - # Fetch - _data = YfData(session=session) - response = _data.post(_SCREENER_URL_, - body=post_query, - user_agent_headers=_data.user_agent_headers, - params=params_dict, - proxy=proxy) - response.raise_for_status() - return response.json()['finance']['result'][0] diff --git a/yfinance/utils.py b/yfinance/utils.py index 201e98789..1600090d6 100644 --- a/yfinance/utils.py +++ b/yfinance/utils.py @@ -45,6 +45,27 @@ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'} +def merge_two_level_dicts(dict1, dict2): + result = dict1.copy() + for key, value in dict2.items(): + if key in result: + # If both are sets, merge them + if isinstance(value, set) and isinstance(result[key], set): + result[key] = result[key] | value + # If both are dicts, merge their contents + elif isinstance(value, dict) and isinstance(result[key], dict): + result[key] = { + k: (result[key].get(k, set()) | v if isinstance(v, set) + else v) if k in result[key] + else v + for k, v in value.items() + } + else: + result[key] = value + return result + + + # From https://stackoverflow.com/a/59128615 def attributes(obj): disallowed_names = { From 588355d490535a7dad855eb841c3f0a52c0091d1 Mon Sep 17 00:00:00 2001 From: R5dan Date: Thu, 30 Jan 2025 12:43:31 +0000 Subject: [PATCH 2/3] fix --- yfinance/__init__.py | 7 ++----- yfinance/screener/__init__.py | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/yfinance/__init__.py b/yfinance/__init__.py index 2f78eb46e..61d498fa0 100644 --- a/yfinance/__init__.py +++ b/yfinance/__init__.py @@ -30,8 +30,7 @@ from .domain.industry import Industry from .domain.market import Market -from .screener.query import EquityQuery, FundQuery -from .screener.screener import screen, PREDEFINED_SCREENER_QUERIES +from . import screener __version__ = version.version __author__ = "Ran Aroussi" @@ -39,6 +38,4 @@ import warnings warnings.filterwarnings('default', category=DeprecationWarning, module='^yfinance') -__all__ = ['download', 'Market', 'Search', 'Ticker', 'Tickers', 'enable_debug_mode', 'set_tz_cache_location', 'Sector', 'Industry'] -# screener stuff: -__all__ += ['EquityQuery', 'FundQuery', 'screen', 'PREDEFINED_SCREENER_QUERIES'] \ No newline at end of file +__all__ = ['download', 'Market', 'Search', 'Ticker', 'Tickers', 'enable_debug_mode', 'set_tz_cache_location', 'Sector', 'Industry', "screener"] \ No newline at end of file diff --git a/yfinance/screener/__init__.py b/yfinance/screener/__init__.py index 97e008503..0a3f1dd9b 100644 --- a/yfinance/screener/__init__.py +++ b/yfinance/screener/__init__.py @@ -1,4 +1,3 @@ -from .query import EquityQuery, screen, FundQuery, Query, QueryHead -from .const import PREDEFINED_SCREENER_QUERIES +from .query import EquityQuery, screen, FundQuery, Query, QueryHead, PREDEFINED_SCREENER_QUERIES __all__ = ["EquityQuery", "FundQuery", "screen", "PREDEFINED_SCREENER_QUERIES", "Query", "QueryHead"] From 81412a6be134ed83d8355936a5d307cde1abb718 Mon Sep 17 00:00:00 2001 From: R5dan Date: Thu, 30 Jan 2025 12:47:46 +0000 Subject: [PATCH 3/3] Add screen to QueryBase --- yfinance/screener/query.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/yfinance/screener/query.py b/yfinance/screener/query.py index c17773749..720fe1e57 100644 --- a/yfinance/screener/query.py +++ b/yfinance/screener/query.py @@ -321,6 +321,19 @@ def __repr__(self, indent=0) -> 'str': def __str__(self) -> 'str': return self.__repr__() + + def screen(self, offset:'int'=None, size:'int'=None, sortField:'str'=None, sortAsc:'bool'=None, userId:'str'=None, userIdType:'str'=None, session=None, proxy=None): + return screen( + self, + offset=offset, + size=size, + sortField=sortField, + sortAsc=sortAsc, + userId=userId, + userIdType=userIdType, + session=session, + proxy=proxy + ) class EquityQuery(QueryBase):