Skip to content

Commit 5bbe358

Browse files
authored
Merge pull request #2182 from ranaroussi/dev
dev -> main
2 parents 38c1323 + 59d0974 commit 5bbe358

File tree

12 files changed

+281
-44
lines changed

12 files changed

+281
-44
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Yahoo! finance API is intended for personal use only.**
3939
- `Ticker`: single ticker data
4040
- `Tickers`: multiple tickers' data
4141
- `download`: download market data for multiple tickers
42+
- `Search`: quotes and news from search
4243
- `Sector` and `Industry`: sector and industry information
4344
- `EquityQuery` and `Screener`: build query to screen market
4445

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import yfinance as yf
2+
3+
# get list of quotes
4+
quotes = yf.Search("AAPL", max_results=10).quotes
5+
6+
# get list of news
7+
news = yf.Search("Google", news_count=10).news

doc/source/reference/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The following are the publicly available classes, and functions exposed by the `
1515

1616
- :attr:`Ticker <yfinance.Ticker>`: Class for accessing single ticker data.
1717
- :attr:`Tickers <yfinance.Tickers>`: Class for handling multiple tickers.
18+
- :attr:`Search <yfinance.Search>`: Class for accessing search results.
1819
- :attr:`Sector <yfinance.Sector>`: Domain class for accessing sector information.
1920
- :attr:`Industry <yfinance.Industry>`: Domain class for accessing industry information.
2021
- :attr:`download <yfinance.download>`: Function to download market data for multiple tickers.
@@ -32,6 +33,7 @@ The following are the publicly available classes, and functions exposed by the `
3233
yfinance.stock
3334
yfinance.financials
3435
yfinance.analysis
36+
yfinance.search
3537
yfinance.sector_industry
3638
yfinance.functions
3739

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
=====================
2+
Search & News
3+
=====================
4+
5+
.. currentmodule:: yfinance
6+
7+
8+
Class
9+
------------
10+
The `Search` module, allows you to access search data in a Pythonic way.
11+
12+
.. autosummary::
13+
:toctree: api/
14+
15+
Search
16+
17+
Search Sample Code
18+
------------------
19+
The `Search` module, allows you to access search data in a Pythonic way.
20+
21+
.. literalinclude:: examples/search.py
22+
:language: python

tests/test_screener.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def setUpClass(self):
1313
self.query = EquityQuery('gt',['eodprice',3])
1414

1515
def test_set_default_body(self):
16-
self.screener.set_default_body(self.query)
16+
result = self.screener.set_default_body(self.query)
1717

1818
self.assertEqual(self.screener.body['offset'], 0)
1919
self.assertEqual(self.screener.body['size'], 100)
@@ -23,11 +23,13 @@ def test_set_default_body(self):
2323
self.assertEqual(self.screener.body['query'], self.query.to_dict())
2424
self.assertEqual(self.screener.body['userId'], '')
2525
self.assertEqual(self.screener.body['userIdType'], 'guid')
26+
self.assertEqual(self.screener, result)
2627

2728
def test_set_predefined_body(self):
2829
k = 'most_actives'
29-
self.screener.set_predefined_body(k)
30+
result = self.screener.set_predefined_body(k)
3031
self.assertEqual(self.screener.body, PREDEFINED_SCREENER_BODY_MAP[k])
32+
self.assertEqual(self.screener, result)
3133

3234
def test_set_predefined_body_invalid_key(self):
3335
with self.assertRaises(ValueError):
@@ -44,9 +46,10 @@ def test_set_body(self):
4446
"userId": "",
4547
"userIdType": "guid"
4648
}
47-
self.screener.set_body(body)
49+
result = self.screener.set_body(body)
4850

4951
self.assertEqual(self.screener.body, body)
52+
self.assertEqual(self.screener, result)
5053

5154
def test_set_body_missing_keys(self):
5255
body = {
@@ -87,10 +90,11 @@ def test_patch_body(self):
8790
}
8891
self.screener.set_body(initial_body)
8992
patch_values = {"size": 50}
90-
self.screener.patch_body(patch_values)
93+
result = self.screener.patch_body(patch_values)
9194

9295
self.assertEqual(self.screener.body['size'], 50)
9396
self.assertEqual(self.screener.body['query'], self.query.to_dict())
97+
self.assertEqual(self.screener, result)
9498

9599
def test_patch_body_extra_keys(self):
96100
initial_body = {
@@ -108,6 +112,22 @@ def test_patch_body_extra_keys(self):
108112
with self.assertRaises(ValueError):
109113
self.screener.patch_body(patch_values)
110114

115+
@patch('yfinance.screener.screener.YfData.post')
116+
def test_set_large_size_in_body(self, mock_post):
117+
body = {
118+
"offset": 0,
119+
"size": 251, # yahoo limits at 250
120+
"sortField": "ticker",
121+
"sortType": "desc",
122+
"quoteType": "equity",
123+
"query": self.query.to_dict(),
124+
"userId": "",
125+
"userIdType": "guid"
126+
}
127+
128+
with self.assertRaises(ValueError):
129+
self.screener.set_body(body).response
130+
111131
@patch('yfinance.screener.screener.YfData.post')
112132
def test_fetch(self, mock_post):
113133
mock_response = MagicMock()

tests/test_search.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import unittest
2+
3+
from tests.context import yfinance as yf
4+
5+
6+
class TestSearch(unittest.TestCase):
7+
def test_valid_query(self):
8+
search = yf.Search(query="AAPL", max_results=5, news_count=3)
9+
10+
self.assertEqual(len(search.quotes), 5)
11+
self.assertEqual(len(search.news), 3)
12+
self.assertIn("AAPL", search.quotes[0]['symbol'])
13+
14+
def test_invalid_query(self):
15+
search = yf.Search(query="XYZXYZ")
16+
17+
self.assertEqual(len(search.quotes), 0)
18+
self.assertEqual(len(search.news), 0)
19+
20+
def test_empty_query(self):
21+
search = yf.Search(query="")
22+
23+
self.assertEqual(len(search.quotes), 0)
24+
self.assertEqual(len(search.news), 0)
25+
26+
def test_fuzzy_query(self):
27+
search = yf.Search(query="Appel", enable_fuzzy_query=True)
28+
29+
# Check if the fuzzy search retrieves relevant results despite the typo
30+
self.assertGreater(len(search.quotes), 0)
31+
self.assertIn("AAPL", search.quotes[0]['symbol'])

yfinance/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#
2121

2222
from . import version
23+
from .search import Search
2324
from .ticker import Ticker
2425
from .tickers import Tickers
2526
from .multi import download
@@ -36,5 +37,5 @@
3637
import warnings
3738
warnings.filterwarnings('default', category=DeprecationWarning, module='^yfinance')
3839

39-
__all__ = ['download', 'Ticker', 'Tickers', 'enable_debug_mode', 'set_tz_cache_location', 'Sector', 'Industry',
40-
'EquityQuery','Screener']
40+
__all__ = ['download', 'Search', 'Ticker', 'Tickers', 'enable_debug_mode', 'set_tz_cache_location', 'Sector',
41+
'Industry', 'EquityQuery', 'Screener']

yfinance/base.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -535,26 +535,45 @@ def get_isin(self, proxy=None) -> Optional[str]:
535535
self._isin = data.split(search_str)[1].split('"')[0].split('|')[0]
536536
return self._isin
537537

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

542-
# Getting data from json
543-
url = f"{_BASE_URL_}/v1/finance/search?q={self.ticker}"
544-
data = self._data.cache_get(url=url, proxy=proxy)
543+
logger = utils.get_yf_logger()
544+
545+
tab_queryrefs = {
546+
"all": "newsAll",
547+
"news": "latestNews",
548+
"press releases": "pressRelease",
549+
}
550+
551+
query_ref = tab_queryrefs.get(tab.lower())
552+
if not query_ref:
553+
raise ValueError(f"Invalid tab name '{tab}'. Choose from: {', '.join(tab_queryrefs.keys())}")
554+
555+
url = f"{_ROOT_URL_}/xhr/ncp?queryRef={query_ref}&serviceKey=ncp_fin"
556+
payload = {
557+
"serviceConfig": {
558+
"snippetCount": count,
559+
"s": [self.ticker]
560+
}
561+
}
562+
563+
data = self._data.post(url, body=payload, proxy=proxy)
545564
if data is None or "Will be right back" in data.text:
546565
raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
547566
"Our engineers are working quickly to resolve "
548567
"the issue. Thank you for your patience.")
549568
try:
550569
data = data.json()
551-
except (_json.JSONDecodeError):
552-
logger = utils.get_yf_logger()
570+
except _json.JSONDecodeError:
553571
logger.error(f"{self.ticker}: Failed to retrieve the news and received faulty response instead.")
554572
data = {}
555573

556-
# parse news
557-
self._news = data.get("news", [])
574+
news = data.get("data", {}).get("tickerStream", {}).get("stream", [])
575+
576+
self._news = [article for article in news if not article.get('ad', [])]
558577
return self._news
559578

560579
@utils.log_indent_decorator

yfinance/multi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
@utils.log_indent_decorator
3838
def download(tickers, start=None, end=None, actions=False, threads=True,
39-
ignore_tz=None, group_by='column', auto_adjust=False, back_adjust=False,
39+
ignore_tz=None, group_by='column', auto_adjust=True, back_adjust=False,
4040
repair=False, keepna=False, progress=True, period="max", interval="1d",
4141
prepost=False, proxy=None, rounding=False, timeout=10, session=None,
4242
multi_level_index=True) -> Union[_pd.DataFrame, None]:
@@ -65,7 +65,7 @@ def download(tickers, start=None, end=None, actions=False, threads=True,
6565
Include Pre and Post market data in results?
6666
Default is False
6767
auto_adjust: bool
68-
Adjust all OHLC automatically? Default is False
68+
Adjust all OHLC automatically? Default is True
6969
repair: bool
7070
Detect currency unit 100x mixups and attempt repair
7171
Default is False

yfinance/screener/screener.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,22 @@ def predefined_bodies(self) -> Dict:
6767
"""
6868
return self._predefined_bodies
6969

70-
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:
70+
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':
7171
"""
72-
Set the default body using a custom query
72+
Set the default body using a custom query.
73+
74+
Args:
75+
query (Query): The Query object to set as the body.
76+
offset (Optional[int]): The offset for the results. Defaults to 0.
77+
size (Optional[int]): The number of results to return. Defaults to 100. Maximum is 250 as set by Yahoo.
78+
sortField (Optional[str]): The field to sort the results by. Defaults to "ticker".
79+
sortType (Optional[str]): The type of sorting (e.g., "asc" or "desc"). Defaults to "desc".
80+
quoteType (Optional[str]): The type of quote (e.g., "equity"). Defaults to "equity".
81+
userId (Optional[str]): The user ID. Defaults to an empty string.
82+
userIdType (Optional[str]): The type of user ID (e.g., "guid"). Defaults to "guid".
83+
84+
Returns:
85+
Screener: self
7386
7487
Example:
7588
@@ -89,11 +102,18 @@ def set_default_body(self, query: Query, offset: int = 0, size: int = 100, sortF
89102
"userId": userId,
90103
"userIdType": userIdType
91104
}
105+
return self
92106

93-
def set_predefined_body(self, k: str) -> None:
107+
def set_predefined_body(self, predefined_key: str) -> 'Screener':
94108
"""
95109
Set a predefined body
96110
111+
Args:
112+
predefined_key (str): key to one of predefined screens
113+
114+
Returns:
115+
Screener: self
116+
97117
Example:
98118
99119
.. code-block:: python
@@ -106,16 +126,23 @@ def set_predefined_body(self, k: str) -> None:
106126
:attr:`Screener.predefined_bodies <yfinance.Screener.predefined_bodies>`
107127
supported predefined screens
108128
"""
109-
body = PREDEFINED_SCREENER_BODY_MAP.get(k, None)
129+
body = PREDEFINED_SCREENER_BODY_MAP.get(predefined_key, None)
110130
if not body:
111-
raise ValueError(f'Invalid key {k} provided for predefined screener')
131+
raise ValueError(f'Invalid key {predefined_key} provided for predefined screener')
112132

113133
self._body_updated = True
114134
self._body = body
135+
return self
115136

116-
def set_body(self, body: Dict) -> None:
137+
def set_body(self, body: Dict) -> 'Screener':
117138
"""
118-
Set the fully custom body
139+
Set the fully custom body using dictionary input
140+
141+
Args:
142+
body (Dict): full query body
143+
144+
Returns:
145+
Screener: self
119146
120147
Example:
121148
@@ -142,11 +169,17 @@ def set_body(self, body: Dict) -> None:
142169

143170
self._body_updated = True
144171
self._body = body
172+
return self
145173

146-
147-
def patch_body(self, values: Dict) -> None:
174+
def patch_body(self, values: Dict) -> 'Screener':
148175
"""
149-
Patch parts of the body
176+
Patch parts of the body using dictionary input
177+
178+
Args:
179+
body (Dict): partial query body
180+
181+
Returns:
182+
Screener: self
150183
151184
Example:
152185
@@ -161,10 +194,14 @@ def patch_body(self, values: Dict) -> None:
161194
self._body_updated = True
162195
for k in values:
163196
self._body[k] = values[k]
197+
return self
164198

165199
def _validate_body(self) -> None:
166200
if not all(k in self._body for k in self._accepted_body_keys):
167201
raise ValueError("Missing required keys in body")
202+
203+
if self._body["size"] > 250:
204+
raise ValueError("Yahoo limits query size to 250. Please decrease the size of the query.")
168205

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

0 commit comments

Comments
 (0)