From 5e9ecc62a9cf2b4b9309096db1d349f052cc8e01 Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Sun, 12 Jan 2025 12:58:35 +0000 Subject: [PATCH] More exchanges. Improve Query docstrings. Tidy predefineds --- yfinance/const.py | 81 ++++++++++++++++++++---- yfinance/screener/query.py | 60 ++++++++++-------- yfinance/screener/screener.py | 16 ++--- yfinance/utils.py | 115 +++++++++++++++++++++++++++++++--- 4 files changed, 218 insertions(+), 54 deletions(-) diff --git a/yfinance/const.py b/yfinance/const.py index 72153b7a..eeea90a5 100644 --- a/yfinance/const.py +++ b/yfinance/const.py @@ -325,18 +325,71 @@ def merge_two_level_dicts(dict1, dict2): result[key] = value return result -COMMON_SCREENER_EQ_MAP = { +EQUITY_SCREENER_EQ_MAP = { "region": { - "za", "ve", "vn", "us", "tw", "th", "tr", "sr", "sg", "sa", "se", "ru", "ro", "qa", "pt", "pk", "pl", - "ph", "nz", "nl", "mx", "pe", "no", "my", "lv", "lt", "kw", "jp", "is", "il", "lk", "kr", "it", "in", - "ie", "hu", "id", "hk", "gb", "fi", "eg", "dk", "gr", "fr", "es", "ee", "de", "cz", "cl", "ca", "be", - "at", "cn", "br", "au", "ar", "ch" + "ar","at","au","be","br","ca","ch","cl","cn","cz","de","dk","ee","eg","es","fi","fr","gb","gr", + "hk","hu","id","ie","il","in","is","it","jp","kr","kw","lk","lt","lv","mx","my","nl","no","nz", + "pe","ph","pk","pl","pt","qa","ro","ru","sa","se","sg","sr","th","tr","tw","us","ve","vn","za" }, "exchange": { - "NMS", "NAS", "YHD", "NYQ", "NGM", "NCM", "BSE" - } -} -EQUITY_SCREENER_EQ_MAP = { + 'ar': {'BUE'}, + 'at': {'VIE'}, + 'au': {'ASX'}, + 'be': {'BRU'}, + 'br': {'SAO'}, + 'ca': {'CNQ', 'TOR', 'VAN'}, + 'ch': {'EBS'}, + 'cl': {'SGO'}, + 'cn': {'SHH', 'SHZ'}, + 'cz': {'PRA'}, + 'de': {'BER', 'DUS', 'FRA', 'HAM', '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': {'KSC'}, + 'kw': {'KUW'}, + 'lk': {}, + 'lt': {'LIT'}, + 'lv': {'RIS'}, + 'mx': {'MEX'}, + 'my': {'KLS'}, + 'nl': {'AMS'}, + 'no': {'OSL'}, + 'nz': {'NZE'}, + 'pe': {}, + 'ph': {}, + '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', 'NCM', 'NGM', 'NMS', 'NYQ', 'OEM', 'OQB', 'OQX', 'PNK', 'YHD'}, + 've': {'CCS'}, + 'vn': {}, + 'za': {'JNB'} + }, "sector": { "Basic Materials", "Industrials", "Communication Services", "Healthcare", "Real Estate", "Technology", "Energy", "Utilities", "Financial Services", @@ -448,9 +501,11 @@ def merge_two_level_dicts(dict1, dict2): "Banks" } } -EQUITY_SCREENER_EQ_MAP = merge_two_level_dicts(EQUITY_SCREENER_EQ_MAP, COMMON_SCREENER_EQ_MAP) -FUND_SCREENER_EQ_MAP = {} -FUND_SCREENER_EQ_MAP = merge_two_level_dicts(FUND_SCREENER_EQ_MAP, COMMON_SCREENER_EQ_MAP) +FUND_SCREENER_EQ_MAP = { + "exchange": { + 'us': {'NAS'} + } +} COMMON_SCREENER_FIELDS = { "price":{ "eodprice", @@ -569,4 +624,4 @@ def merge_two_level_dicts(dict1, dict2): "social_score", "highest_controversy"} } -EQUITY_SCREENER_FIELDS = merge_two_level_dicts(EQUITY_SCREENER_FIELDS, COMMON_SCREENER_FIELDS) \ No newline at end of file +EQUITY_SCREENER_FIELDS = merge_two_level_dicts(EQUITY_SCREENER_FIELDS, COMMON_SCREENER_FIELDS) diff --git a/yfinance/screener/query.py b/yfinance/screener/query.py index fbf8cca6..51f0d564 100644 --- a/yfinance/screener/query.py +++ b/yfinance/screener/query.py @@ -5,7 +5,7 @@ from yfinance.const import EQUITY_SCREENER_EQ_MAP, EQUITY_SCREENER_FIELDS from yfinance.const import FUND_SCREENER_EQ_MAP, FUND_SCREENER_FIELDS from yfinance.exceptions import YFNotImplementedError -from ..utils import dynamic_docstring, generate_list_table_from_dict +from ..utils import dynamic_docstring, generate_list_table_from_dict_universal class QueryBase(ABC): @@ -56,7 +56,12 @@ def _validate_eq_operand(self, operand: List[Union[str, numbers.Real]]) -> None: 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: - if operand[1] not in self.valid_values[operand[0]]: + 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()]) + 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: @@ -84,8 +89,13 @@ def _validate_isin_operand(self, operand: List['QueryBase']) -> None: 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 self.valid_values[operand[0]]: + if operand[i] not in vv: raise ValueError(f'Invalid EQ value "{operand[i]}"') def to_dict(self) -> Dict: @@ -101,25 +111,25 @@ def to_dict(self) -> Dict: "operands": [operand.to_dict() if isinstance(operand, type(self)) else operand for operand in self.operands] } - def __repr__(self, root=True) -> str: - s = '"' if root else '' - - if root: - # Only print type at start, more compact. - s += f"{type(self).__name__}" - s += f"({self.operator}, [" - for i in range(len(self.operands)): - o = self.operands[i] - if isinstance(o, QueryBase): - s += o.__repr__(root=False) + def __repr__(self, indent=0) -> str: + indent_str = " " * indent + class_name = self.__class__.__name__ + + if isinstance(self.operands, list): + # For list operands, check if they contain any QueryBase objects + if any(isinstance(op, QueryBase) for op in self.operands): + # If there are nested queries, format them with newlines + operands_str = ",\n".join( + f"{indent_str} {op.__repr__(indent + 1) if isinstance(op, QueryBase) else repr(op)}" + for op in self.operands + ) + return f"{class_name}({self.operator}, [\n{operands_str}\n{indent_str}])" else: - s += o.__repr__() - if i < len(self.operands)-1: - s += ', ' - s += ']' - if root: - s += '"' - return s + # For lists of simple types, keep them on one line + return f"{class_name}({self.operator}, {repr(self.operands)})" + else: + # Handle single operand + return f"{class_name}({self.operator}, {repr(self.operands)})" def __str__(self) -> str: return self.__repr__() @@ -149,7 +159,7 @@ class EquityQuery(QueryBase): ]) """ - @dynamic_docstring({"valid_operand_fields_table": generate_list_table_from_dict(EQUITY_SCREENER_FIELDS)}) + @dynamic_docstring({"valid_operand_fields_table": generate_list_table_from_dict_universal(EQUITY_SCREENER_FIELDS)}) @property def valid_fields(self) -> Dict: """ @@ -158,7 +168,7 @@ def valid_fields(self) -> Dict: """ return EQUITY_SCREENER_FIELDS - @dynamic_docstring({"valid_values_table": generate_list_table_from_dict(EQUITY_SCREENER_EQ_MAP)}) + @dynamic_docstring({"valid_values_table": generate_list_table_from_dict_universal(EQUITY_SCREENER_EQ_MAP, concat_keys=['exchange'])}) @property def valid_values(self) -> Dict: """ @@ -191,7 +201,7 @@ class FundQuery(QueryBase): FundQuery('eq', ['exchange', 'NAS']) ]) """ - @dynamic_docstring({"valid_operand_fields_table": generate_list_table_from_dict(FUND_SCREENER_FIELDS)}) + @dynamic_docstring({"valid_operand_fields_table": generate_list_table_from_dict_universal(FUND_SCREENER_FIELDS)}) @property def valid_fields(self) -> Dict: """ @@ -200,7 +210,7 @@ def valid_fields(self) -> Dict: """ return FUND_SCREENER_FIELDS - @dynamic_docstring({"valid_values_table": generate_list_table_from_dict(FUND_SCREENER_EQ_MAP)}) + @dynamic_docstring({"valid_values_table": generate_list_table_from_dict_universal(FUND_SCREENER_EQ_MAP)}) @property def valid_values(self) -> Dict: """ diff --git a/yfinance/screener/screener.py b/yfinance/screener/screener.py index dbadc0cf..278d22eb 100644 --- a/yfinance/screener/screener.py +++ b/yfinance/screener/screener.py @@ -5,7 +5,7 @@ from yfinance.const import _BASE_URL_ from yfinance.data import YfData -from ..utils import dynamic_docstring, generate_list_table_from_dict_of_dict +from ..utils import dynamic_docstring, generate_list_table_from_dict_universal from typing import Union @@ -18,19 +18,19 @@ PREDEFINED_SCREENER_BODY_MAP = { '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", "quoteType":"EQUITY", + '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", "quoteType":"EQUITY", + '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", "quoteType":"EQUITY", + '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", "quoteType":"EQUITY", + '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":{"operator":"and", "operands":[{"operator":"lt", "operands":["intradaymarketcap",2000000000]},{"operator":"or", "operands":[{"operator":"eq", "operands":["exchange", "NMS"]},{"operator":"eq", "operands":["exchange", "NYQ"]}]}]}}, - 'undervalued_growth_stocks': {"sortType":"DESC", "sortField":"eodvolume", "quoteType":"EQUITY", + "query": EqyQy("and", [EqyQy("lt", ["intradaymarketcap",2000000000]), EqyQy("or", [EqyQy("eq", ["exchange", "NMS"]), EqyQy("eq", ["exchange", "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'])])}, @@ -48,7 +48,7 @@ "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_of_dict(PREDEFINED_SCREENER_BODY_MAP, bullets=False, title='Predefined queries')}) +@dynamic_docstring({"predefined_screeners": generate_list_table_from_dict_universal(PREDEFINED_SCREENER_BODY_MAP, bullets=True, title='Predefined queries')}) def screen(query: Union[str, QueryBase], offset: int = 0, size: int = 25, diff --git a/yfinance/utils.py b/yfinance/utils.py index 57443b5e..a8e33c06 100644 --- a/yfinance/utils.py +++ b/yfinance/utils.py @@ -976,19 +976,118 @@ def generate_list_table_from_dict(data: dict, bullets: bool=True, title: str=Non table += ' '*5 + f"- {value_str}\n" return table -def generate_list_table_from_dict_of_dict(data: dict, bullets: bool=True, title: str=None) -> str: +# def generate_list_table_from_dict_of_dict(data: dict, bullets: bool=True, title: str=None) -> str: +# """ +# Generate a list-table for the docstring showing permitted keys/values. +# """ +# table = _generate_table_configurations(title) +# for k in sorted(data.keys()): +# values = data[k] +# table += ' '*3 + f"* - {k}\n" +# if bullets: +# table += ' '*5 + "-\n" +# for value in sorted(values): +# table += ' '*7 + f"- {value}\n" +# else: +# table += ' '*5 + f"- {values}\n" +# return table + + +def generate_list_table_from_dict_universal(data: dict, bullets: bool=True, title: str=None, concat_keys=[]) -> str: """ Generate a list-table for the docstring showing permitted keys/values. """ table = _generate_table_configurations(title) - for k in sorted(data.keys()): + for k in data.keys(): values = data[k] - value_str = values + table += ' '*3 + f"* - {k}\n" - if bullets: - table += ' '*5 + "-\n" - for value in sorted(values): - table += ' '*7 + f"- {value}\n" + if isinstance(values, dict): + table_add = '' + + concat_short_lines = k in concat_keys + + if bullets: + k_keys = sorted(list(values.keys())) + current_line = '' + block_format = 'query' in k_keys + for i in range(len(k_keys)): + k2 = k_keys[i] + k2_values = values[k2] + k2_values_str = None + if isinstance(k2_values, set): + k2_values = list(k2_values) + elif isinstance(k2_values, dict) and len(k2_values) == 0: + k2_values = [] + if isinstance(k2_values, list): + k2_values = sorted(k2_values) + all_scalar = all(isinstance(k2v, (int, float, str)) for k2v in k2_values) + if all_scalar: + k2_values_str = _re.sub(r"[{}\[\]']", "", str(k2_values)) + + if k2_values_str is None: + k2_values_str = str(k2_values) + + if len(current_line) > 0 and (len(current_line) + len(k2_values_str) > 40): + # new line + table_add += current_line + '\n' + current_line = '' + + if concat_short_lines: + if current_line == '': + current_line += ' '*5 + if i == 0: + # Only add dash to first + current_line += f"- " + else: + current_line += f" " + # current_line += '* ' + # Don't draw bullet points + current_line += '| ' + else: + current_line += ', ' + current_line += f"{k2}: " + k2_values_str + else: + table_add += ' '*5 + if i == 0: + # Only add dash to first + table_add += f"- " + else: + table_add += f" " + + if '\n' in k2_values_str: + # Block format multiple lines + table_add += '| ' + f"{k2}:: " + "\n" + k2_values_str_lines = k2_values_str.split('\n') + for j in range(len(k2_values_str_lines)): + l = k2_values_str_lines[j] + table_add += ' '*7 + '|' + ' '*5 + l + if j < len(k2_values_str_lines)-1: + table_add += "\n" + else: + # table_add += '* ' + if block_format: + table_add += '| ' + else: + table_add += '* ' + table_add += f"{k2}: " + k2_values_str + + table_add += "\n" + if current_line != '': + table_add += current_line + '\n' + else: + table_add += ' '*5 + f"- {values}\n" + + table += table_add + else: - table += ' '*5 + f"- {value_str}\n" + lengths = [len(str(v)) for v in values] + if bullets and max(lengths) > 5: + table += ' '*5 + "-\n" + for value in sorted(values): + table += ' '*7 + f"- {value}\n" + else: + value_str = ', '.join(sorted(values)) + table += ' '*5 + f"- {value_str}\n" + return table \ No newline at end of file