Add valuation measures
This commit is contained in:
@@ -19,6 +19,7 @@ from yfinance.config import YfConfig
|
||||
|
||||
import unittest
|
||||
# import requests_cache
|
||||
from unittest.mock import patch, MagicMock
|
||||
from typing import Union, Any, get_args, _GenericAlias
|
||||
# from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||||
|
||||
@@ -1235,6 +1236,54 @@ class TestTickerFundsData(unittest.TestCase):
|
||||
sector_weightings = ticker.funds_data.sector_weightings
|
||||
self.assertIsInstance(sector_weightings, dict)
|
||||
|
||||
class TestTickerValuationMeasures(unittest.TestCase):
|
||||
|
||||
_MOCK_HTML = """<html><body>
|
||||
<table>
|
||||
<tr><td></td><td>Current</td><td>12/31/2025</td><td>9/30/2025</td></tr>
|
||||
<tr><td>Market Cap</td><td>3.76T</td><td>4.00T</td><td>3.76T</td></tr>
|
||||
<tr><td>Enterprise Value</td><td>3.78T</td><td>4.04T</td><td>3.81T</td></tr>
|
||||
<tr><td>Trailing P/E</td><td>32.39</td><td>36.44</td><td>38.64</td></tr>
|
||||
<tr><td>Forward P/E</td><td>29.76</td><td>32.79</td><td>31.65</td></tr>
|
||||
<tr><td>PEG Ratio (5yr expected)</td><td>2.27</td><td>2.75</td><td>2.44</td></tr>
|
||||
<tr><td>Price/Sales</td><td>8.77</td><td>9.80</td><td>9.41</td></tr>
|
||||
<tr><td>Price/Book</td><td>42.60</td><td>54.21</td><td>57.14</td></tr>
|
||||
<tr><td>Enterprise Value/Revenue</td><td>8.68</td><td>9.71</td><td>9.32</td></tr>
|
||||
<tr><td>Enterprise Value/EBITDA</td><td>24.73</td><td>27.92</td><td>26.87</td></tr>
|
||||
</table>
|
||||
</body></html>"""
|
||||
|
||||
def _make_ticker_with_mock(self, html):
|
||||
mock_response = MagicMock()
|
||||
mock_response.text = html
|
||||
with patch("yfinance.data.YfData.cache_get", return_value=mock_response):
|
||||
dat = yf.Ticker("AAPL")
|
||||
data = dat.valuation_measures
|
||||
return data
|
||||
|
||||
def test_valuation_measures(self):
|
||||
data = self._make_ticker_with_mock(self._MOCK_HTML)
|
||||
self.assertEqual(data.shape, (9, 3), "unexpected shape")
|
||||
self.assertListEqual(list(data.columns), ["Current", "12/31/2025", "9/30/2025"])
|
||||
self.assertIn("Market Cap", data.index)
|
||||
self.assertIn("Trailing P/E", data.index)
|
||||
self.assertIn("Enterprise Value/EBITDA", data.index)
|
||||
self.assertIsNone(data.index.name)
|
||||
self.assertEqual(data.loc["Market Cap", "Current"], "3.76T")
|
||||
self.assertEqual(data.loc["Forward P/E", "12/31/2025"], "32.79")
|
||||
|
||||
def test_valuation_measures_no_table(self):
|
||||
data = self._make_ticker_with_mock("<html><body><p>No tables here</p></body></html>")
|
||||
self.assertIsInstance(data, pd.DataFrame)
|
||||
self.assertTrue(data.empty)
|
||||
|
||||
def test_valuation_measures_fetch_error(self):
|
||||
with patch("yfinance.data.YfData.cache_get", side_effect=Exception("network error")):
|
||||
dat = yf.Ticker("AAPL")
|
||||
data = dat.valuation_measures
|
||||
self.assertIsInstance(data, pd.DataFrame)
|
||||
self.assertTrue(data.empty)
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(TestTicker('Test ticker'))
|
||||
@@ -1244,6 +1293,7 @@ def suite():
|
||||
suite.addTest(TestTickerMiscFinancials('Test misc financials'))
|
||||
suite.addTest(TestTickerInfo('Test info & fast_info'))
|
||||
suite.addTest(TestTickerFundsData('Test Funds Data'))
|
||||
suite.addTest(TestTickerValuationMeasures('Test valuation measures'))
|
||||
return suite
|
||||
|
||||
|
||||
|
||||
@@ -287,6 +287,10 @@ class TickerBase:
|
||||
self._fast_info = FastInfo(self)
|
||||
return self._fast_info
|
||||
|
||||
def get_valuation_measures(self):
|
||||
data = self._quote.valuation_measures
|
||||
return data
|
||||
|
||||
def get_sustainability(self, as_dict=False):
|
||||
data = self._quote.sustainability
|
||||
if as_dict:
|
||||
|
||||
@@ -3,6 +3,7 @@ import datetime
|
||||
import json
|
||||
import numpy as _np
|
||||
import pandas as pd
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from yfinance import utils
|
||||
from yfinance.config import YfConfig
|
||||
@@ -492,6 +493,7 @@ class Quote:
|
||||
self._upgrades_downgrades = None
|
||||
self._calendar = None
|
||||
self._sec_filings = None
|
||||
self._valuation_measures = None
|
||||
|
||||
self._already_scraped = False
|
||||
self._already_fetched = False
|
||||
@@ -572,6 +574,12 @@ class Quote:
|
||||
self._sec_filings = {} if f is None else f
|
||||
return self._sec_filings
|
||||
|
||||
@property
|
||||
def valuation_measures(self) -> pd.DataFrame:
|
||||
if self._valuation_measures is None:
|
||||
self._fetch_valuation_measures()
|
||||
return self._valuation_measures
|
||||
|
||||
@staticmethod
|
||||
def valid_modules():
|
||||
return quote_summary_valid_modules
|
||||
@@ -661,6 +669,40 @@ class Quote:
|
||||
|
||||
self._info = {k: _format(k, v) for k, v in query1_info.items()}
|
||||
|
||||
def _fetch_valuation_measures(self):
|
||||
url = f"https://finance.yahoo.com/quote/{self._symbol}/key-statistics"
|
||||
try:
|
||||
response = self._data.cache_get(url=url)
|
||||
except Exception as e:
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
utils.get_yf_logger().error(f"Failed to fetch key-statistics page: {e}")
|
||||
self._valuation_measures = pd.DataFrame()
|
||||
return
|
||||
|
||||
try:
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
table = soup.find("table")
|
||||
if table is None:
|
||||
self._valuation_measures = pd.DataFrame()
|
||||
return
|
||||
|
||||
headers = [th.get_text(strip=True) for th in table.find("tr").find_all(["th", "td"])]
|
||||
rows = []
|
||||
for tr in table.find_all("tr")[1:]:
|
||||
cells = [td.get_text(strip=True) for td in tr.find_all(["th", "td"])]
|
||||
rows.append(cells)
|
||||
|
||||
df = pd.DataFrame(rows, columns=headers)
|
||||
df = df.set_index(df.columns[0])
|
||||
df.index.name = None
|
||||
self._valuation_measures = df
|
||||
except Exception as e:
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
utils.get_yf_logger().error(f"Failed to parse key-statistics page: {e}")
|
||||
self._valuation_measures = pd.DataFrame()
|
||||
|
||||
def _fetch_complementary(self):
|
||||
if self._already_fetched_complementary:
|
||||
return
|
||||
|
||||
@@ -162,6 +162,10 @@ class Ticker(TickerBase):
|
||||
def fast_info(self):
|
||||
return self.get_fast_info()
|
||||
|
||||
@property
|
||||
def valuation(self) -> _pd.DataFrame:
|
||||
return self.get_valuation_measures()
|
||||
|
||||
@property
|
||||
def calendar(self) -> dict:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user