Import python venv for stability
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
from .query import EquityQuery
|
||||
from .screener import screen, PREDEFINED_SCREENER_QUERIES
|
||||
|
||||
__all__ = ['EquityQuery', 'FundQuery', 'screen', 'PREDEFINED_SCREENER_QUERIES']
|
||||
@@ -0,0 +1,218 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import numbers
|
||||
from typing import List, Union, Dict, TypeVar, Tuple
|
||||
|
||||
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_universal
|
||||
|
||||
T = TypeVar('T', bound=Union[str, numbers.Real])
|
||||
|
||||
class QueryBase(ABC):
|
||||
def __init__(self, operator: str, operand: Union[ List['QueryBase'], Tuple[str, Tuple[Union[str, numbers.Real], ...]] ]):
|
||||
operator = operator.upper()
|
||||
|
||||
if not isinstance(operand, list):
|
||||
raise TypeError('Invalid operand type')
|
||||
if len(operand) <= 0:
|
||||
raise ValueError('Invalid field for EquityQuery')
|
||||
|
||||
if operator == 'IS-IN':
|
||||
self._validate_isin_operand(operand)
|
||||
elif operator in {'OR','AND'}:
|
||||
self._validate_or_and_operand(operand)
|
||||
elif operator == 'EQ':
|
||||
self._validate_eq_operand(operand)
|
||||
elif operator == 'BTWN':
|
||||
self._validate_btwn_operand(operand)
|
||||
elif operator in {'GT','LT','GTE','LTE'}:
|
||||
self._validate_gt_lt(operand)
|
||||
else:
|
||||
raise ValueError('Invalid Operator Value')
|
||||
|
||||
self.operator = operator
|
||||
self.operands = operand
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def valid_fields(self) -> List:
|
||||
raise YFNotImplementedError('valid_fields() needs to be implemented by child')
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
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:
|
||||
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:
|
||||
if len(operand) != 2:
|
||||
raise ValueError('Operand must be length 2 for EQ')
|
||||
|
||||
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()])
|
||||
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:
|
||||
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()):
|
||||
raise ValueError(f'Invalid field for {type(self)}')
|
||||
if isinstance(operand[1], numbers.Real) is False:
|
||||
raise TypeError('Invalid comparison type for BTWN')
|
||||
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:
|
||||
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()):
|
||||
raise ValueError(f'Invalid field for {type(self)} "{operand[0]}"')
|
||||
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:
|
||||
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]
|
||||
}
|
||||
|
||||
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:
|
||||
# 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__()
|
||||
|
||||
|
||||
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).
|
||||
|
||||
Combine them with logical operations: `AND`, `OR`.
|
||||
|
||||
Example:
|
||||
Predefined Yahoo query `aggressive_small_caps`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from yfinance import EquityQuery
|
||||
|
||||
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:
|
||||
"""
|
||||
Valid operands, grouped by category.
|
||||
{valid_operand_fields_table}
|
||||
"""
|
||||
return EQUITY_SCREENER_FIELDS
|
||||
|
||||
@dynamic_docstring({"valid_values_table": generate_list_table_from_dict_universal(EQUITY_SCREENER_EQ_MAP, concat_keys=['exchange', 'industry'])})
|
||||
@property
|
||||
def valid_values(self) -> Dict:
|
||||
"""
|
||||
Most operands take number values, but some have a restricted set of valid values.
|
||||
{valid_values_table}
|
||||
"""
|
||||
return EQUITY_SCREENER_EQ_MAP
|
||||
|
||||
|
||||
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).
|
||||
|
||||
Combine them with logical operations: `AND`, `OR`.
|
||||
|
||||
Example:
|
||||
Predefined Yahoo query `solid_large_growth_funds`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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'])
|
||||
])
|
||||
"""
|
||||
@dynamic_docstring({"valid_operand_fields_table": generate_list_table_from_dict_universal(FUND_SCREENER_FIELDS)})
|
||||
@property
|
||||
def valid_fields(self) -> Dict:
|
||||
"""
|
||||
Valid operands, grouped by category.
|
||||
{valid_operand_fields_table}
|
||||
"""
|
||||
return FUND_SCREENER_FIELDS
|
||||
|
||||
@dynamic_docstring({"valid_values_table": generate_list_table_from_dict_universal(FUND_SCREENER_EQ_MAP)})
|
||||
@property
|
||||
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
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import curl_cffi
|
||||
from typing import Union
|
||||
import warnings
|
||||
from json import dumps
|
||||
|
||||
from yfinance.const import _QUERY1_URL_
|
||||
from yfinance.data import YfData
|
||||
from ..utils import dynamic_docstring, generate_list_table_from_dict_universal
|
||||
|
||||
from .query import EquityQuery as EqyQy
|
||||
from .query import FundQuery as FndQy
|
||||
from .query import QueryBase, EquityQuery, FundQuery
|
||||
|
||||
_SCREENER_URL_ = f"{_QUERY1_URL_}/v1/finance/screener"
|
||||
_PREDEFINED_URL_ = f"{_SCREENER_URL_}/predefined/saved"
|
||||
|
||||
PREDEFINED_SCREENER_BODY_DEFAULTS = {
|
||||
"offset":0, "count":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': {"count":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,
|
||||
count: int = None,
|
||||
sortField: str = None,
|
||||
sortAsc: bool = None,
|
||||
userId: str = None,
|
||||
userIdType: str = None,
|
||||
session = 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)
|
||||
Use count instead for predefined queries.
|
||||
count : int
|
||||
number of results to return. Default 25, maximum 250 (Yahoo)
|
||||
Use size instead for custom queries.
|
||||
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}
|
||||
"""
|
||||
|
||||
_data = YfData(session=session)
|
||||
|
||||
# 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,
|
||||
'count': 25,
|
||||
'sortField': 'ticker',
|
||||
'sortAsc': False,
|
||||
'userId': "",
|
||||
'userIdType': "guid"
|
||||
}
|
||||
|
||||
if count is not None and count > 250:
|
||||
raise ValueError("Yahoo limits query count to 250, reduce count.")
|
||||
|
||||
if size is not None and size > 250:
|
||||
raise ValueError("Yahoo limits query size to 250, reduce size.")
|
||||
|
||||
if offset is not None and isinstance(query, str):
|
||||
# offset ignored by predefined API so switch to other API
|
||||
post_query = PREDEFINED_SCREENER_QUERIES[query]
|
||||
query = post_query['query']
|
||||
# use predefined's attributes if user not specified
|
||||
if sortField is None:
|
||||
sortField = post_query['sortField']
|
||||
if sortAsc is None:
|
||||
sortAsc = post_query['sortType'].lower() == 'asc'
|
||||
# and don't use defaults
|
||||
defaults = {}
|
||||
|
||||
fields = {'offset': offset, 'count': count, "size": size, 'sortField': sortField, 'sortAsc': sortAsc, 'userId': userId, 'userIdType': userIdType}
|
||||
|
||||
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
|
||||
|
||||
if size is not None:
|
||||
warnings.warn("Screen 'size' argument is deprecated for predefined screens, set 'count' instead.", DeprecationWarning, stacklevel=2)
|
||||
count = size
|
||||
size = None
|
||||
fields['count'] = fields['size']
|
||||
del fields['size']
|
||||
|
||||
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)
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except curl_cffi.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()
|
||||
data = dumps(post_query, separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
# Fetch
|
||||
response = _data.post(_SCREENER_URL_,
|
||||
data=data,
|
||||
params=params_dict)
|
||||
response.raise_for_status()
|
||||
return response.json()['finance']['result'][0]
|
||||
Reference in New Issue
Block a user