Skip to content

Commit

Permalink
More exchanges. Improve Query docstrings. Tidy predefineds
Browse files Browse the repository at this point in the history
  • Loading branch information
ValueRaider committed Jan 12, 2025
1 parent 4cc9f0c commit 5e9ecc6
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 54 deletions.
81 changes: 68 additions & 13 deletions yfinance/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
EQUITY_SCREENER_FIELDS = merge_two_level_dicts(EQUITY_SCREENER_FIELDS, COMMON_SCREENER_FIELDS)
60 changes: 35 additions & 25 deletions yfinance/screener/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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__()
Expand Down Expand Up @@ -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:
"""
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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:
"""
Expand All @@ -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:
"""
Expand Down
16 changes: 8 additions & 8 deletions yfinance/screener/screener.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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'])])},
Expand All @@ -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,
Expand Down
115 changes: 107 additions & 8 deletions yfinance/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 5e9ecc6

Please sign in to comment.