Skip to content

Commit

Permalink
Merge pull request #2160 from dhruvan2006/feature/search
Browse files Browse the repository at this point in the history
Feature: Add search feature
  • Loading branch information
ValueRaider authored Dec 6, 2024
2 parents f526464 + 873ab0f commit 3ac8539
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 36 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
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']
25 changes: 9 additions & 16 deletions yfinance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import pandas as pd
import requests

from . import utils, cache
from . import utils, cache, Search
from .data import YfData
from .exceptions import YFEarningsDateMissing
from .scrapers.analysis import Analysis
Expand Down Expand Up @@ -538,22 +538,15 @@ def get_news(self, proxy=None) -> list:
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)
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()
logger.error(f"{self.ticker}: Failed to retrieve the news and received faulty response instead.")
data = {}
search = Search(
query=self.ticker,
news_count=10,
session=self.session,
proxy=proxy,
raise_errors=True
)
self._news = search.news

# parse news
self._news = data.get("news", [])
return self._news

@utils.log_indent_decorator
Expand Down
95 changes: 95 additions & 0 deletions yfinance/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# yfinance - market data downloader
# https://github.com/ranaroussi/yfinance
#
# Copyright 2017-2019 Ran Aroussi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import json as _json

from . import utils
from .const import _BASE_URL_
from .data import YfData


class Search:
def __init__(self, query, max_results=8, news_count=8, enable_fuzzy_query=False,
session=None, proxy=None, timeout=30, raise_errors=True):
"""
Fetches and organizes search results from Yahoo Finance, including stock quotes and news articles.
Args:
query: The search query (ticker symbol or company name).
max_results: Maximum number of stock quotes to return (default 8).
news_count: Number of news articles to include (default 8).
enable_fuzzy_query: Enable fuzzy search for typos (default False).
session: Custom HTTP session for requests (default None).
proxy: Proxy settings for requests (default None).
timeout: Request timeout in seconds (default 30).
raise_errors: Raise exceptions on error (default True).
"""
self.query = query
self.max_results = max_results
self.enable_fuzzy_query = enable_fuzzy_query
self.news_count = news_count
self.session = session
self.proxy = proxy
self.timeout = timeout
self.raise_errors = raise_errors

self._data = YfData(session=self.session)
self._logger = utils.get_yf_logger()

self._response = self._fetch_results()
self._quotes = self._response.get("quotes", [])
self._news = self._response.get("news", [])

def _fetch_results(self):
url = f"{_BASE_URL_}/v1/finance/search"
params = {
"q": self.query,
"quotesCount": self.max_results,
"enableFuzzyQuery": self.enable_fuzzy_query,
"newsCount": self.news_count,
"quotesQueryId": "tss_match_phrase_query",
"newsQueryId": "news_cie_vespa"
}

self._logger.debug(f'{self.query}: Yahoo GET parameters: {str(dict(params))}')

data = self._data.cache_get(url=url, params=params, proxy=self.proxy, timeout=self.timeout)
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:
self._logger.error(f"{self.query}: Failed to retrieve the news and received faulty response instead.")
data = {}

return data

@property
def quotes(self):
"""Get the quotes from the search results."""
return self._quotes

@property
def news(self):
"""Get the news from the search results."""
return self._news
38 changes: 20 additions & 18 deletions yfinance/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
from pytz import UnknownTimeZoneError

from yfinance import const
from .const import _BASE_URL_

user_agent_headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'}
Expand Down Expand Up @@ -189,24 +188,27 @@ def is_isin(string):
def get_all_by_isin(isin, proxy=None, session=None):
if not (is_isin(isin)):
raise ValueError("Invalid ISIN number")

# Deferred this to prevent circular imports
from .search import Search

session = session or _requests
url = f"{_BASE_URL_}/v1/finance/search?q={isin}"
data = session.get(url=url, proxies=proxy, headers=user_agent_headers)
try:
data = data.json()
ticker = data.get('quotes', [{}])[0]
return {
'ticker': {
'symbol': ticker['symbol'],
'shortname': ticker['shortname'],
'longname': ticker.get('longname',''),
'type': ticker['quoteType'],
'exchange': ticker['exchDisp'],
},
'news': data.get('news', [])
}
except Exception:
return {}
search = Search(query=isin, max_results=1, session=session, proxy=proxy)

# Extract the first quote and news
ticker = search.quotes[0] if search.quotes else {}
news = search.news

return {
'ticker': {
'symbol': ticker.get('symbol', ''),
'shortname': ticker.get('shortname', ''),
'longname': ticker.get('longname', ''),
'type': ticker.get('quoteType', ''),
'exchange': ticker.get('exchDisp', ''),
},
'news': news
}


def get_ticker_by_isin(isin, proxy=None, session=None):
Expand Down

0 comments on commit 3ac8539

Please sign in to comment.