Skip to content

Commit

Permalink
Merge pull request #2182 from ranaroussi/dev
Browse files Browse the repository at this point in the history
dev -> main
  • Loading branch information
ValueRaider authored Dec 19, 2024
2 parents 38c1323 + 59d0974 commit 5bbe358
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 44 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Yahoo! finance API is intended for personal use only.**
- `Ticker`: single ticker data
- `Tickers`: multiple tickers' data
- `download`: download market data for multiple tickers
- `Search`: quotes and news from search
- `Sector` and `Industry`: sector and industry information
- `EquityQuery` and `Screener`: build query to screen market

Expand Down
7 changes: 7 additions & 0 deletions doc/source/reference/examples/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import yfinance as yf

# get list of quotes
quotes = yf.Search("AAPL", max_results=10).quotes

# get list of news
news = yf.Search("Google", news_count=10).news
2 changes: 2 additions & 0 deletions doc/source/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The following are the publicly available classes, and functions exposed by the `

- :attr:`Ticker <yfinance.Ticker>`: Class for accessing single ticker data.
- :attr:`Tickers <yfinance.Tickers>`: Class for handling multiple tickers.
- :attr:`Search <yfinance.Search>`: Class for accessing search results.
- :attr:`Sector <yfinance.Sector>`: Domain class for accessing sector information.
- :attr:`Industry <yfinance.Industry>`: Domain class for accessing industry information.
- :attr:`download <yfinance.download>`: Function to download market data for multiple tickers.
Expand All @@ -32,6 +33,7 @@ The following are the publicly available classes, and functions exposed by the `
yfinance.stock
yfinance.financials
yfinance.analysis
yfinance.search
yfinance.sector_industry
yfinance.functions

Expand Down
22 changes: 22 additions & 0 deletions doc/source/reference/yfinance.search.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
=====================
Search & News
=====================

.. currentmodule:: yfinance


Class
------------
The `Search` module, allows you to access search data in a Pythonic way.

.. autosummary::
:toctree: api/

Search

Search Sample Code
------------------
The `Search` module, allows you to access search data in a Pythonic way.

.. literalinclude:: examples/search.py
:language: python
28 changes: 24 additions & 4 deletions tests/test_screener.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def setUpClass(self):
self.query = EquityQuery('gt',['eodprice',3])

def test_set_default_body(self):
self.screener.set_default_body(self.query)
result = self.screener.set_default_body(self.query)

self.assertEqual(self.screener.body['offset'], 0)
self.assertEqual(self.screener.body['size'], 100)
Expand All @@ -23,11 +23,13 @@ def test_set_default_body(self):
self.assertEqual(self.screener.body['query'], self.query.to_dict())
self.assertEqual(self.screener.body['userId'], '')
self.assertEqual(self.screener.body['userIdType'], 'guid')
self.assertEqual(self.screener, result)

def test_set_predefined_body(self):
k = 'most_actives'
self.screener.set_predefined_body(k)
result = self.screener.set_predefined_body(k)
self.assertEqual(self.screener.body, PREDEFINED_SCREENER_BODY_MAP[k])
self.assertEqual(self.screener, result)

def test_set_predefined_body_invalid_key(self):
with self.assertRaises(ValueError):
Expand All @@ -44,9 +46,10 @@ def test_set_body(self):
"userId": "",
"userIdType": "guid"
}
self.screener.set_body(body)
result = self.screener.set_body(body)

self.assertEqual(self.screener.body, body)
self.assertEqual(self.screener, result)

def test_set_body_missing_keys(self):
body = {
Expand Down Expand Up @@ -87,10 +90,11 @@ def test_patch_body(self):
}
self.screener.set_body(initial_body)
patch_values = {"size": 50}
self.screener.patch_body(patch_values)
result = self.screener.patch_body(patch_values)

self.assertEqual(self.screener.body['size'], 50)
self.assertEqual(self.screener.body['query'], self.query.to_dict())
self.assertEqual(self.screener, result)

def test_patch_body_extra_keys(self):
initial_body = {
Expand All @@ -108,6 +112,22 @@ def test_patch_body_extra_keys(self):
with self.assertRaises(ValueError):
self.screener.patch_body(patch_values)

@patch('yfinance.screener.screener.YfData.post')
def test_set_large_size_in_body(self, mock_post):
body = {
"offset": 0,
"size": 251, # yahoo limits at 250
"sortField": "ticker",
"sortType": "desc",
"quoteType": "equity",
"query": self.query.to_dict(),
"userId": "",
"userIdType": "guid"
}

with self.assertRaises(ValueError):
self.screener.set_body(body).response

@patch('yfinance.screener.screener.YfData.post')
def test_fetch(self, mock_post):
mock_response = MagicMock()
Expand Down
31 changes: 31 additions & 0 deletions tests/test_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import unittest

from tests.context import yfinance as yf


class TestSearch(unittest.TestCase):
def test_valid_query(self):
search = yf.Search(query="AAPL", max_results=5, news_count=3)

self.assertEqual(len(search.quotes), 5)
self.assertEqual(len(search.news), 3)
self.assertIn("AAPL", search.quotes[0]['symbol'])

def test_invalid_query(self):
search = yf.Search(query="XYZXYZ")

self.assertEqual(len(search.quotes), 0)
self.assertEqual(len(search.news), 0)

def test_empty_query(self):
search = yf.Search(query="")

self.assertEqual(len(search.quotes), 0)
self.assertEqual(len(search.news), 0)

def test_fuzzy_query(self):
search = yf.Search(query="Appel", enable_fuzzy_query=True)

# Check if the fuzzy search retrieves relevant results despite the typo
self.assertGreater(len(search.quotes), 0)
self.assertIn("AAPL", search.quotes[0]['symbol'])
5 changes: 3 additions & 2 deletions yfinance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#

from . import version
from .search import Search
from .ticker import Ticker
from .tickers import Tickers
from .multi import download
Expand All @@ -36,5 +37,5 @@
import warnings
warnings.filterwarnings('default', category=DeprecationWarning, module='^yfinance')

__all__ = ['download', 'Ticker', 'Tickers', 'enable_debug_mode', 'set_tz_cache_location', 'Sector', 'Industry',
'EquityQuery','Screener']
__all__ = ['download', 'Search', 'Ticker', 'Tickers', 'enable_debug_mode', 'set_tz_cache_location', 'Sector',
'Industry', 'EquityQuery', 'Screener']
35 changes: 27 additions & 8 deletions yfinance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,26 +535,45 @@ def get_isin(self, proxy=None) -> Optional[str]:
self._isin = data.split(search_str)[1].split('"')[0].split('|')[0]
return self._isin

def get_news(self, proxy=None) -> list:
def get_news(self, count=10, tab="news", proxy=None) -> list:
"""Allowed options for tab: "news", "all", "press releases"""
if self._news:
return self._news

# Getting data from json
url = f"{_BASE_URL_}/v1/finance/search?q={self.ticker}"
data = self._data.cache_get(url=url, proxy=proxy)
logger = utils.get_yf_logger()

tab_queryrefs = {
"all": "newsAll",
"news": "latestNews",
"press releases": "pressRelease",
}

query_ref = tab_queryrefs.get(tab.lower())
if not query_ref:
raise ValueError(f"Invalid tab name '{tab}'. Choose from: {', '.join(tab_queryrefs.keys())}")

url = f"{_ROOT_URL_}/xhr/ncp?queryRef={query_ref}&serviceKey=ncp_fin"
payload = {
"serviceConfig": {
"snippetCount": count,
"s": [self.ticker]
}
}

data = self._data.post(url, body=payload, proxy=proxy)
if data is None or "Will be right back" in data.text:
raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
"Our engineers are working quickly to resolve "
"the issue. Thank you for your patience.")
try:
data = data.json()
except (_json.JSONDecodeError):
logger = utils.get_yf_logger()
except _json.JSONDecodeError:
logger.error(f"{self.ticker}: Failed to retrieve the news and received faulty response instead.")
data = {}

# parse news
self._news = data.get("news", [])
news = data.get("data", {}).get("tickerStream", {}).get("stream", [])

self._news = [article for article in news if not article.get('ad', [])]
return self._news

@utils.log_indent_decorator
Expand Down
4 changes: 2 additions & 2 deletions yfinance/multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

@utils.log_indent_decorator
def download(tickers, start=None, end=None, actions=False, threads=True,
ignore_tz=None, group_by='column', auto_adjust=False, back_adjust=False,
ignore_tz=None, group_by='column', auto_adjust=True, back_adjust=False,
repair=False, keepna=False, progress=True, period="max", interval="1d",
prepost=False, proxy=None, rounding=False, timeout=10, session=None,
multi_level_index=True) -> Union[_pd.DataFrame, None]:
Expand Down Expand Up @@ -65,7 +65,7 @@ def download(tickers, start=None, end=None, actions=False, threads=True,
Include Pre and Post market data in results?
Default is False
auto_adjust: bool
Adjust all OHLC automatically? Default is False
Adjust all OHLC automatically? Default is True
repair: bool
Detect currency unit 100x mixups and attempt repair
Default is False
Expand Down
57 changes: 47 additions & 10 deletions yfinance/screener/screener.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,22 @@ def predefined_bodies(self) -> Dict:
"""
return self._predefined_bodies

def set_default_body(self, query: Query, offset: int = 0, size: int = 100, sortField: str = "ticker", sortType: str = "desc", quoteType: str = "equity", userId: str = "", userIdType: str = "guid") -> None:
def set_default_body(self, query: Query, offset: int = 0, size: int = 100, sortField: str = "ticker", sortType: str = "desc", quoteType: str = "equity", userId: str = "", userIdType: str = "guid") -> 'Screener':
"""
Set the default body using a custom query
Set the default body using a custom query.
Args:
query (Query): The Query object to set as the body.
offset (Optional[int]): The offset for the results. Defaults to 0.
size (Optional[int]): The number of results to return. Defaults to 100. Maximum is 250 as set by Yahoo.
sortField (Optional[str]): The field to sort the results by. Defaults to "ticker".
sortType (Optional[str]): The type of sorting (e.g., "asc" or "desc"). Defaults to "desc".
quoteType (Optional[str]): The type of quote (e.g., "equity"). Defaults to "equity".
userId (Optional[str]): The user ID. Defaults to an empty string.
userIdType (Optional[str]): The type of user ID (e.g., "guid"). Defaults to "guid".
Returns:
Screener: self
Example:
Expand All @@ -89,11 +102,18 @@ def set_default_body(self, query: Query, offset: int = 0, size: int = 100, sortF
"userId": userId,
"userIdType": userIdType
}
return self

def set_predefined_body(self, k: str) -> None:
def set_predefined_body(self, predefined_key: str) -> 'Screener':
"""
Set a predefined body
Args:
predefined_key (str): key to one of predefined screens
Returns:
Screener: self
Example:
.. code-block:: python
Expand All @@ -106,16 +126,23 @@ def set_predefined_body(self, k: str) -> None:
:attr:`Screener.predefined_bodies <yfinance.Screener.predefined_bodies>`
supported predefined screens
"""
body = PREDEFINED_SCREENER_BODY_MAP.get(k, None)
body = PREDEFINED_SCREENER_BODY_MAP.get(predefined_key, None)
if not body:
raise ValueError(f'Invalid key {k} provided for predefined screener')
raise ValueError(f'Invalid key {predefined_key} provided for predefined screener')

self._body_updated = True
self._body = body
return self

def set_body(self, body: Dict) -> None:
def set_body(self, body: Dict) -> 'Screener':
"""
Set the fully custom body
Set the fully custom body using dictionary input
Args:
body (Dict): full query body
Returns:
Screener: self
Example:
Expand All @@ -142,11 +169,17 @@ def set_body(self, body: Dict) -> None:

self._body_updated = True
self._body = body
return self


def patch_body(self, values: Dict) -> None:
def patch_body(self, values: Dict) -> 'Screener':
"""
Patch parts of the body
Patch parts of the body using dictionary input
Args:
body (Dict): partial query body
Returns:
Screener: self
Example:
Expand All @@ -161,10 +194,14 @@ def patch_body(self, values: Dict) -> None:
self._body_updated = True
for k in values:
self._body[k] = values[k]
return self

def _validate_body(self) -> None:
if not all(k in self._body for k in self._accepted_body_keys):
raise ValueError("Missing required keys in body")

if self._body["size"] > 250:
raise ValueError("Yahoo limits query size to 250. Please decrease the size of the query.")

def _fetch(self) -> Dict:
params_dict = {"corsDomain": "finance.yahoo.com", "formatted": "false", "lang": "en-US", "region": "US"}
Expand Down
Loading

0 comments on commit 5bbe358

Please sign in to comment.