c3db565d09
reindex_dfs() rebuilt the combined index via sorted(set(...)) +
pd.to_datetime, dropping the index name set by history.py ('Date' for
daily, 'Datetime' for intraday). After concat, data.index.name was
None, so users doing data.stack().reset_index() got a 'level_0' column
instead of 'Date' / 'Datetime'.
Use Index.union, which preserves the name natively when sources agree.
Drive-by: narrow data.columns to MultiIndex before swaplevel to silence
a pyright attribute warning.
1351 lines
55 KiB
Python
1351 lines
55 KiB
Python
"""
|
|
Tests for Ticker
|
|
|
|
To run all tests in suite from commandline:
|
|
python -m unittest tests.ticker
|
|
|
|
Specific test class:
|
|
python -m unittest tests.ticker.TestTicker
|
|
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
|
|
import pandas as pd
|
|
|
|
from tests.context import yfinance as yf
|
|
from tests.context import session_gbl
|
|
from yfinance.exceptions import YFPricesMissingError, YFInvalidPeriodError, YFNotImplementedError, YFTickerMissingError, YFTzMissingError, YFDataException
|
|
from yfinance.config import YfConfig
|
|
|
|
import unittest
|
|
# import requests_cache
|
|
from unittest.mock import patch, MagicMock
|
|
from typing import Union, Any, get_args, get_origin
|
|
# from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
|
|
ticker_attributes = (
|
|
("major_holders", pd.DataFrame),
|
|
("institutional_holders", pd.DataFrame),
|
|
("mutualfund_holders", pd.DataFrame),
|
|
("insider_transactions", pd.DataFrame),
|
|
("insider_purchases", pd.DataFrame),
|
|
("insider_roster_holders", pd.DataFrame),
|
|
("splits", pd.Series),
|
|
("actions", pd.DataFrame),
|
|
("shares", pd.DataFrame),
|
|
("info", dict),
|
|
("calendar", dict),
|
|
("recommendations", Union[pd.DataFrame, dict]),
|
|
("recommendations_summary", Union[pd.DataFrame, dict]),
|
|
("upgrades_downgrades", Union[pd.DataFrame, dict]),
|
|
("ttm_cashflow", pd.DataFrame),
|
|
("quarterly_cashflow", pd.DataFrame),
|
|
("cashflow", pd.DataFrame),
|
|
("quarterly_balance_sheet", pd.DataFrame),
|
|
("balance_sheet", pd.DataFrame),
|
|
("ttm_income_stmt", pd.DataFrame),
|
|
("quarterly_income_stmt", pd.DataFrame),
|
|
("income_stmt", pd.DataFrame),
|
|
("analyst_price_targets", dict),
|
|
("earnings_estimate", pd.DataFrame),
|
|
("revenue_estimate", pd.DataFrame),
|
|
("earnings_history", pd.DataFrame),
|
|
("eps_trend", pd.DataFrame),
|
|
("eps_revisions", pd.DataFrame),
|
|
("growth_estimates", pd.DataFrame),
|
|
("sustainability", pd.DataFrame),
|
|
("options", tuple),
|
|
("news", Any),
|
|
("earnings_dates", pd.DataFrame),
|
|
)
|
|
|
|
def assert_attribute_type(testClass: unittest.TestCase, instance, attribute_name, expected_type):
|
|
try:
|
|
attribute = getattr(instance, attribute_name)
|
|
except YFNotImplementedError:
|
|
# Some attributes legitimately raise on missing/bad tickers.
|
|
return
|
|
|
|
if attribute is not None and expected_type is not Any:
|
|
err_msg = f'{attribute_name} type is {type(attribute)} not {expected_type}'
|
|
if get_origin(expected_type) is Union:
|
|
allowed_types = get_args(expected_type)
|
|
testClass.assertTrue(isinstance(attribute, allowed_types), err_msg)
|
|
else:
|
|
testClass.assertEqual(type(attribute), expected_type, err_msg)
|
|
|
|
|
|
class TestTicker(unittest.TestCase):
|
|
session = None
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.session = session_gbl
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
if cls.session is not None:
|
|
cls.session.close()
|
|
|
|
def tearDown(self):
|
|
YfConfig.debug.hide_exceptions = True
|
|
|
|
def test_getTz(self):
|
|
tkrs = ["IMP.JO", "BHG.JO", "SSW.JO", "BP.L", "INTC"]
|
|
for tkr in tkrs:
|
|
# First step: remove ticker from tz-cache
|
|
yf.cache.get_tz_cache().store(tkr, None)
|
|
|
|
# Test:
|
|
dat = yf.Ticker(tkr, session=self.session)
|
|
tz = dat._get_ticker_tz(timeout=5)
|
|
|
|
self.assertIsNotNone(tz)
|
|
|
|
def test_badTicker(self):
|
|
# Check yfinance doesn't die when ticker delisted
|
|
|
|
tkr = "DJI" # typo of "^DJI"
|
|
dat = yf.Ticker(tkr, session=self.session)
|
|
|
|
dat.history(period="5d")
|
|
dat.history(start="2022-01-01")
|
|
dat.history(start="2022-01-01", end="2022-03-01")
|
|
yf.download([tkr], period="5d", threads=False, ignore_tz=False)
|
|
yf.download([tkr], period="5d", threads=True, ignore_tz=False)
|
|
yf.download([tkr], period="5d", threads=False, ignore_tz=True)
|
|
yf.download([tkr], period="5d", threads=True, ignore_tz=True)
|
|
|
|
for k in dat.fast_info:
|
|
dat.fast_info[k]
|
|
|
|
for attribute_name, attribute_type in ticker_attributes:
|
|
assert_attribute_type(self, dat, attribute_name, attribute_type)
|
|
|
|
assert isinstance(dat.dividends, pd.Series)
|
|
assert dat.dividends.empty
|
|
assert isinstance(dat.splits, pd.Series)
|
|
assert dat.splits.empty
|
|
assert isinstance(dat.capital_gains, pd.Series)
|
|
assert dat.capital_gains.empty
|
|
with self.assertRaises(YFNotImplementedError):
|
|
assert isinstance(dat.shares, pd.DataFrame)
|
|
assert dat.shares.empty
|
|
assert isinstance(dat.actions, pd.DataFrame)
|
|
assert dat.actions.empty
|
|
|
|
tkr = '6623.N'
|
|
dat = yf.Ticker(tkr, session=self.session)
|
|
dat.get_dividends(period="1y")
|
|
dat.get_splits(period="1y")
|
|
dat.get_capital_gains(period="1y")
|
|
|
|
def test_invalid_period(self):
|
|
tkr = 'VALE'
|
|
dat = yf.Ticker(tkr, session=self.session)
|
|
YfConfig.debug.hide_exceptions = False
|
|
with self.assertRaises(YFInvalidPeriodError):
|
|
dat.history(period="2wks", interval="1d")
|
|
with self.assertRaises(YFInvalidPeriodError):
|
|
dat.history(period="2mos", interval="1d")
|
|
|
|
def test_valid_custom_periods(self):
|
|
valid_periods = [
|
|
# Yahoo provided periods
|
|
("1d", "1m"), ("5d", "15m"), ("1mo", "1d"), ("3mo", "1wk"),
|
|
("6mo", "1d"), ("1y", "1mo"), ("5y", "1wk"), ("max", "1mo"),
|
|
|
|
# Custom periods
|
|
("2d", "30m"), ("10mo", "1d"), ("1y", "1d"), ("3y", "1d"),
|
|
("2wk", "15m"), ("6mo", "5d"), ("10y", "1wk")
|
|
]
|
|
|
|
tkr = "AAPL"
|
|
dat = yf.Ticker(tkr, session=self.session)
|
|
|
|
YfConfig.debug.hide_exceptions = False
|
|
|
|
for period, interval in valid_periods:
|
|
with self.subTest(period=period, interval=interval):
|
|
df = dat.history(period=period, interval=interval)
|
|
self.assertIsInstance(df, pd.DataFrame)
|
|
self.assertFalse(df.empty, f"No data returned for period={period}, interval={interval}")
|
|
self.assertIn("Close", df.columns, f"'Close' column missing for period={period}, interval={interval}")
|
|
|
|
# Validate date range
|
|
now = datetime.now()
|
|
if period != "max": # Difficult to assert for "max", therefore we skip
|
|
if period.endswith("d"):
|
|
days = int(period[:-1])
|
|
expected_start = now - timedelta(days=days)
|
|
elif period.endswith("mo"):
|
|
months = int(period[:-2])
|
|
expected_start = now - timedelta(days=30 * months)
|
|
elif period.endswith("y"):
|
|
years = int(period[:-1])
|
|
expected_start = now - timedelta(days=365 * years)
|
|
elif period.endswith("wk"):
|
|
weeks = int(period[:-2])
|
|
expected_start = now - timedelta(weeks=weeks)
|
|
else:
|
|
continue
|
|
|
|
actual_start = df.index[0].to_pydatetime().replace(tzinfo=None)
|
|
expected_start = expected_start.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
# leeway added because of weekends
|
|
self.assertGreaterEqual(actual_start, expected_start - timedelta(days=10),
|
|
f"Start date {actual_start} out of range for period={period}")
|
|
self.assertLessEqual(df.index[-1].to_pydatetime().replace(tzinfo=None), now,
|
|
f"End date {df.index[-1]} out of range for period={period}")
|
|
|
|
# # 2025-12-11: test failing and no time to find new tkr
|
|
# def test_prices_missing(self):
|
|
# # this test will need to be updated every time someone wants to run a test
|
|
# # hard to find a ticker that matches this error other than options
|
|
# # META call option, 2024 April 26th @ strike of 180000
|
|
|
|
# tkr = 'META240426C00180000'
|
|
# dat = yf.Ticker(tkr, session=self.session)
|
|
# YfConfig.debug.hide_exceptions = False
|
|
# with self.assertRaises(YFPricesMissingError):
|
|
# dat.history(period="5d", interval="1m")
|
|
|
|
def test_ticker_missing(self):
|
|
tkr = 'ATVI'
|
|
dat = yf.Ticker(tkr, session=self.session)
|
|
# A missing ticker can trigger either a niche error or the generalized error
|
|
with self.assertRaises((YFTickerMissingError, YFTzMissingError, YFPricesMissingError)):
|
|
YfConfig.debug.hide_exceptions = False
|
|
dat.history(period="3mo", interval="1d")
|
|
|
|
def test_goodTicker(self):
|
|
# that yfinance works when full api is called on same instance of ticker
|
|
|
|
tkrs = ["IBM"]
|
|
tkrs.append("QCSTIX") # weird ticker, no price history but has previous close
|
|
for tkr in tkrs:
|
|
dat = yf.Ticker(tkr, session=self.session)
|
|
|
|
dat.history(period="5d")
|
|
dat.history(start="2022-01-01")
|
|
dat.history(start="2022-01-01", end="2022-03-01")
|
|
yf.download([tkr], period="5d", threads=False, ignore_tz=False)
|
|
yf.download([tkr], period="5d", threads=True, ignore_tz=False)
|
|
yf.download([tkr], period="5d", threads=False, ignore_tz=True)
|
|
yf.download([tkr], period="5d", threads=True, ignore_tz=True)
|
|
|
|
for k in dat.fast_info:
|
|
dat.fast_info[k]
|
|
|
|
for attribute_name, attribute_type in ticker_attributes:
|
|
assert_attribute_type(self, dat, attribute_name, attribute_type)
|
|
|
|
def test_goodTicker_withProxy(self):
|
|
tkr = "IBM"
|
|
dat = yf.Ticker(tkr, session=self.session)
|
|
|
|
dat._fetch_ticker_tz(timeout=5)
|
|
dat._get_ticker_tz(timeout=5)
|
|
dat.history(period="5d")
|
|
|
|
for attribute_name, attribute_type in ticker_attributes:
|
|
assert_attribute_type(self, dat, attribute_name, attribute_type)
|
|
|
|
def test_ticker_with_symbol_mic(self):
|
|
equities = [
|
|
("OR", "XPAR"), # L'Oréal on Euronext Paris
|
|
("AAPL", "XNYS"), # Apple on NYSE
|
|
("GOOGL", "XNAS"), # Alphabet on NASDAQ
|
|
("BMW", "XETR"), # BMW on XETRA
|
|
]
|
|
for eq in equities:
|
|
# No exception = pass
|
|
yf.Ticker(eq)
|
|
yf.Ticker((eq[0], eq[1].lower()))
|
|
|
|
def test_ticker_with_symbol_mic_invalid(self):
|
|
with self.assertRaises(ValueError) as cm:
|
|
yf.Ticker(('ABC', 'XXXX'))
|
|
self.assertIn("Unknown MIC code: 'XXXX'", str(cm.exception))
|
|
|
|
|
|
class TestTickerHistory(unittest.TestCase):
|
|
session = None
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.session = session_gbl
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
if cls.session is not None:
|
|
cls.session.close()
|
|
|
|
def setUp(self):
|
|
# use a ticker that has dividends
|
|
self.symbol = "IBM"
|
|
self.ticker = yf.Ticker(self.symbol, session=self.session)
|
|
|
|
self.symbols = ["AMZN", "MSFT", "NVDA"]
|
|
|
|
def tearDown(self):
|
|
self.ticker = None
|
|
|
|
def test_history(self):
|
|
md = self.ticker.history_metadata
|
|
self.assertIn("IBM", md.values(), "metadata missing")
|
|
data = self.ticker.history("1y")
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
def test_history_metadata(self):
|
|
# Mainly testing that if user requested price repair,
|
|
# that any metadata refetch also uses repaired data.
|
|
self.ticker.history("1mo", repair=True)
|
|
# - metadata will be fetched because prefers intraday fetch
|
|
md = self.ticker.history_metadata
|
|
self.assertTrue(md['YF repair?'])
|
|
|
|
def test_download(self):
|
|
tomorrow = pd.Timestamp.now().date() + pd.Timedelta(days=1) # helps with caching
|
|
for threads in [False, True]:
|
|
for ignore_tz in [False, True]:
|
|
for mli in [False, True]:
|
|
for n in [1, 'all']:
|
|
for interval in ['1d', '1h']:
|
|
if n == 1:
|
|
symbols = self.symbols[0]
|
|
else:
|
|
# Add some other countries
|
|
symbols = self.symbols + ['BATS.L', '7974.T']
|
|
data = yf.download(symbols, end=tomorrow, session=self.session,
|
|
threads=threads, ignore_tz=ignore_tz, multi_level_index=mli,
|
|
interval=interval, progress=False)
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
expected_name = 'Datetime' if interval[-1] in ('m', 'h') else 'Date'
|
|
self.assertEqual(data.index.name, expected_name)
|
|
if ignore_tz:
|
|
self.assertIsNone(data.index.tz)
|
|
else:
|
|
self.assertIsNotNone(data.index.tz)
|
|
self.assertEqual(str(data.index.tz), "America/New_York")
|
|
if (not mli) and n == 1:
|
|
self.assertFalse(isinstance(data.columns, pd.MultiIndex))
|
|
else:
|
|
self.assertIsInstance(data.columns, pd.MultiIndex)
|
|
|
|
if interval == '1d':
|
|
if ignore_tz:
|
|
self.assertTrue((data.index.hour == 0).all())
|
|
self.assertTrue((data.index.minute == 0).all())
|
|
else:
|
|
self.assertTrue((data.index.minute == 0).all())
|
|
if n == 1:
|
|
self.assertTrue((data.index.hour == 0).all())
|
|
else:
|
|
self.assertTrue((data.index.hour != 0).any())
|
|
elif interval == '1h':
|
|
hours = pd.Index(data.index.hour)
|
|
if ignore_tz:
|
|
self.assertTrue(((hours >= 7) & (hours <= 19)).all())
|
|
else:
|
|
if n == 1:
|
|
self.assertTrue(((hours >= 7) & (hours <= 19)).all())
|
|
else:
|
|
self.assertTrue((~((hours >= 7) & (hours <= 19))).any())
|
|
|
|
# Hopefully one day we find an equivalent "requests_cache" that works with "curl_cffi"
|
|
# def test_no_expensive_calls_introduced(self):
|
|
# """
|
|
# Make sure calling history to get price data has not introduced more calls to yahoo than absolutely necessary.
|
|
# As doing other type of scraping calls than "query2.finance.yahoo.com/v8/finance/chart" to yahoo website
|
|
# will quickly trigger spam-block when doing bulk download of history data.
|
|
# """
|
|
# symbol = "GOOGL"
|
|
# period = "1y"
|
|
# with requests_cache.CachedSession(backend="memory") as session:
|
|
# ticker = yf.Ticker(symbol, session=session)
|
|
# ticker.history(period=period)
|
|
# actual_urls_called = [r.url for r in session.cache.filter()]
|
|
|
|
# # Remove 'crumb' argument
|
|
# for i in range(len(actual_urls_called)):
|
|
# u = actual_urls_called[i]
|
|
# parsed_url = urlparse(u)
|
|
# query_params = parse_qs(parsed_url.query)
|
|
# query_params.pop('crumb', None)
|
|
# query_params.pop('cookie', None)
|
|
# u = urlunparse(parsed_url._replace(query=urlencode(query_params, doseq=True)))
|
|
# actual_urls_called[i] = u
|
|
# actual_urls_called = tuple(actual_urls_called)
|
|
|
|
# expected_urls = [
|
|
# f"https://query2.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1d&range=1d", # ticker's tz
|
|
# f"https://query2.finance.yahoo.com/v8/finance/chart/{symbol}?events=div%2Csplits%2CcapitalGains&includePrePost=False&interval=1d&range={period}"
|
|
# ]
|
|
# for url in actual_urls_called:
|
|
# self.assertTrue(url in expected_urls, f"Unexpected URL called: {url}")
|
|
|
|
def test_dividends(self):
|
|
data = self.ticker.dividends
|
|
self.assertIsInstance(data, pd.Series, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
def test_splits(self):
|
|
data = self.ticker.splits
|
|
self.assertIsInstance(data, pd.Series, "data has wrong type")
|
|
# self.assertFalse(data.empty, "data is empty")
|
|
|
|
def test_actions(self):
|
|
data = self.ticker.actions
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
def test_chained_history_calls(self):
|
|
_ = self.ticker.history(period="2d")
|
|
data = self.ticker.dividends
|
|
self.assertIsInstance(data, pd.Series, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
|
|
class TestTickerEarnings(unittest.TestCase):
|
|
session = None
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.session = session_gbl
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
if cls.session is not None:
|
|
cls.session.close()
|
|
|
|
def setUp(self):
|
|
self.ticker = yf.Ticker("GOOGL", session=self.session)
|
|
|
|
def tearDown(self):
|
|
self.ticker = None
|
|
|
|
def test_earnings_dates(self):
|
|
data = self.ticker.earnings_dates
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
def test_earnings_dates_with_limit(self):
|
|
# use ticker with lots of historic earnings
|
|
ticker = yf.Ticker("IBM")
|
|
limit = 100
|
|
data = ticker.get_earnings_dates(limit=limit)
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
self.assertEqual(len(data), limit, "Wrong number or rows")
|
|
|
|
data_cached = ticker.get_earnings_dates(limit=limit)
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
# Below will fail because not ported to Yahoo API
|
|
|
|
# def test_earnings_forecasts(self):
|
|
# data = self.ticker.earnings_forecasts
|
|
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
# self.assertFalse(data.empty, "data is empty")
|
|
|
|
# data_cached = self.ticker.earnings_forecasts
|
|
# self.assertIs(data, data_cached, "data not cached")
|
|
|
|
# data_cached = self.ticker.earnings_dates
|
|
# self.assertIs(data, data_cached, "data not cached")
|
|
|
|
# def test_earnings_trend(self):
|
|
# data = self.ticker.earnings_trend
|
|
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
# self.assertFalse(data.empty, "data is empty")
|
|
|
|
# data_cached = self.ticker.earnings_trend
|
|
# self.assertIs(data, data_cached, "data not cached")
|
|
|
|
|
|
class TestTickerHolders(unittest.TestCase):
|
|
session = None
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.session = session_gbl
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
if cls.session is not None:
|
|
cls.session.close()
|
|
|
|
def setUp(self):
|
|
self.ticker = yf.Ticker("GOOGL", session=self.session)
|
|
|
|
def tearDown(self):
|
|
self.ticker = None
|
|
|
|
def test_major_holders(self):
|
|
data = self.ticker.major_holders
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.major_holders
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_institutional_holders(self):
|
|
data = self.ticker.institutional_holders
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.institutional_holders
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_mutualfund_holders(self):
|
|
data = self.ticker.mutualfund_holders
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.mutualfund_holders
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_insider_transactions(self):
|
|
data = self.ticker.insider_transactions
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.insider_transactions
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_insider_purchases(self):
|
|
data = self.ticker.insider_purchases
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.insider_purchases
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_insider_roster_holders(self):
|
|
data = self.ticker.insider_roster_holders
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.insider_roster_holders
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
|
|
class TestTickerMiscFinancials(unittest.TestCase):
|
|
session = None
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.session = session_gbl
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
if cls.session is not None:
|
|
cls.session.close()
|
|
|
|
def setUp(self):
|
|
self.ticker = yf.Ticker("GOOGL", session=self.session)
|
|
|
|
# For ticker 'BSE.AX' (and others), Yahoo not returning
|
|
# full quarterly financials (usually cash-flow) with all entries,
|
|
# instead returns a smaller version in different data store.
|
|
self.ticker_old_fmt = yf.Ticker("BSE.AX", session=self.session)
|
|
|
|
def tearDown(self):
|
|
self.ticker = None
|
|
|
|
def test_isin(self):
|
|
data = self.ticker.isin
|
|
self.assertIsInstance(data, str, "data has wrong type")
|
|
self.assertEqual("CA02080M1005", data, "data is empty")
|
|
|
|
data_cached = self.ticker.isin
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_options(self):
|
|
data = self.ticker.options
|
|
self.assertIsInstance(data, tuple, "data has wrong type")
|
|
self.assertTrue(len(data) > 1, "data is empty")
|
|
|
|
def test_shares_full(self):
|
|
data = self.ticker.get_shares_full()
|
|
self.assertIsInstance(data, pd.Series, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
def test_income_statement(self):
|
|
expected_keys = ["Total Revenue", "Basic EPS"]
|
|
expected_periods_days = 365
|
|
|
|
# Test contents of table
|
|
data = self.ticker.get_income_stmt(pretty=True)
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
period = abs((data.columns[0]-data.columns[1]).days)
|
|
self.assertLess(abs(period-expected_periods_days), 20, "Not returning annual financials")
|
|
|
|
# Test property defaults
|
|
data2 = self.ticker.income_stmt
|
|
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")
|
|
|
|
# Test pretty=False
|
|
expected_keys = [k.replace(' ', '') for k in expected_keys]
|
|
data = self.ticker.get_income_stmt(pretty=False)
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
|
|
# Test to_dict
|
|
data = self.ticker.get_income_stmt(as_dict=True)
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
|
|
def test_quarterly_income_statement(self):
|
|
expected_keys = ["Total Revenue", "Basic EPS"]
|
|
expected_periods_days = 365//4
|
|
|
|
# Test contents of table
|
|
data = self.ticker.get_income_stmt(pretty=True, freq="quarterly")
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
period = abs((data.columns[0]-data.columns[1]).days)
|
|
self.assertLess(abs(period-expected_periods_days), 20, "Not returning quarterly financials")
|
|
|
|
# Test property defaults
|
|
data2 = self.ticker.quarterly_income_stmt
|
|
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")
|
|
|
|
# Test pretty=False
|
|
expected_keys = [k.replace(' ', '') for k in expected_keys]
|
|
data = self.ticker.get_income_stmt(pretty=False, freq="quarterly")
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
|
|
# Test to_dict
|
|
data = self.ticker.get_income_stmt(as_dict=True)
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
|
|
def test_ttm_income_statement(self):
|
|
expected_keys = ["Total Revenue", "Pretax Income", "Normalized EBITDA"]
|
|
|
|
# Test contents of table
|
|
data = self.ticker.get_income_stmt(pretty=True, freq='trailing')
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
# Trailing 12 months there must be exactly one column
|
|
self.assertEqual(len(data.columns), 1, "Only one column should be returned on TTM income statement")
|
|
|
|
# Test property defaults
|
|
data2 = self.ticker.ttm_income_stmt
|
|
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")
|
|
|
|
# Test pretty=False
|
|
expected_keys = [k.replace(' ', '') for k in expected_keys]
|
|
data = self.ticker.get_income_stmt(pretty=False, freq='trailing')
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
|
|
# Test to_dict
|
|
data = self.ticker.get_income_stmt(as_dict=True, freq='trailing')
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
|
|
def test_balance_sheet(self):
|
|
expected_keys = ["Total Assets", "Net PPE"]
|
|
expected_periods_days = 365
|
|
|
|
# Test contents of table
|
|
data = self.ticker.get_balance_sheet(pretty=True)
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
period = abs((data.columns[0]-data.columns[1]).days)
|
|
self.assertLess(abs(period-expected_periods_days), 20, "Not returning annual financials")
|
|
|
|
# Test property defaults
|
|
data2 = self.ticker.balance_sheet
|
|
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")
|
|
|
|
# Test pretty=False
|
|
expected_keys = [k.replace(' ', '') for k in expected_keys]
|
|
data = self.ticker.get_balance_sheet(pretty=False)
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
|
|
# Test to_dict
|
|
data = self.ticker.get_balance_sheet(as_dict=True)
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
|
|
def test_quarterly_balance_sheet(self):
|
|
expected_keys = ["Total Assets", "Net PPE"]
|
|
expected_periods_days = 365//4
|
|
|
|
# Test contents of table
|
|
data = self.ticker.get_balance_sheet(pretty=True, freq="quarterly")
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
period = abs((data.columns[0]-data.columns[1]).days)
|
|
self.assertLess(abs(period-expected_periods_days), 20, "Not returning quarterly financials")
|
|
|
|
# Test property defaults
|
|
data2 = self.ticker.quarterly_balance_sheet
|
|
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")
|
|
|
|
# Test pretty=False
|
|
expected_keys = [k.replace(' ', '') for k in expected_keys]
|
|
data = self.ticker.get_balance_sheet(pretty=False, freq="quarterly")
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
|
|
# Test to_dict
|
|
data = self.ticker.get_balance_sheet(as_dict=True, freq="quarterly")
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
|
|
def test_cash_flow(self):
|
|
expected_keys = ["Operating Cash Flow", "Net PPE Purchase And Sale"]
|
|
expected_periods_days = 365
|
|
|
|
# Test contents of table
|
|
data = self.ticker.get_cashflow(pretty=True)
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
period = abs((data.columns[0]-data.columns[1]).days)
|
|
self.assertLess(abs(period-expected_periods_days), 20, "Not returning annual financials")
|
|
|
|
# Test property defaults
|
|
data2 = self.ticker.cashflow
|
|
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")
|
|
|
|
# Test pretty=False
|
|
expected_keys = [k.replace(' ', '') for k in expected_keys]
|
|
data = self.ticker.get_cashflow(pretty=False)
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
|
|
# Test to_dict
|
|
data = self.ticker.get_cashflow(as_dict=True)
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
|
|
def test_quarterly_cash_flow(self):
|
|
expected_keys = ["Operating Cash Flow", "Net PPE Purchase And Sale"]
|
|
expected_periods_days = 365//4
|
|
|
|
# Test contents of table
|
|
data = self.ticker.get_cashflow(pretty=True, freq="quarterly")
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
period = abs((data.columns[0]-data.columns[1]).days)
|
|
self.assertLess(abs(period-expected_periods_days), 20, "Not returning quarterly financials")
|
|
|
|
# Test property defaults
|
|
data2 = self.ticker.quarterly_cashflow
|
|
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")
|
|
|
|
# Test pretty=False
|
|
expected_keys = [k.replace(' ', '') for k in expected_keys]
|
|
data = self.ticker.get_cashflow(pretty=False, freq="quarterly")
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
|
|
# Test to_dict
|
|
data = self.ticker.get_cashflow(as_dict=True)
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
|
|
def test_ttm_cash_flow(self):
|
|
expected_keys = ["Operating Cash Flow", "Net PPE Purchase And Sale"]
|
|
|
|
# Test contents of table
|
|
data = self.ticker.get_cashflow(pretty=True, freq='trailing')
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
# Trailing 12 months there must be exactly one column
|
|
self.assertEqual(len(data.columns), 1, "Only one column should be returned on TTM cash flow")
|
|
|
|
# Test property defaults
|
|
data2 = self.ticker.ttm_cashflow
|
|
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")
|
|
|
|
# Test pretty=False
|
|
expected_keys = [k.replace(' ', '') for k in expected_keys]
|
|
data = self.ticker.get_cashflow(pretty=False, freq='trailing')
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
for k in expected_keys:
|
|
self.assertIn(k, data.index, "Did not find expected row in index")
|
|
|
|
# Test to_dict
|
|
data = self.ticker.get_cashflow(as_dict=True, freq='trailing')
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
|
|
def test_income_alt_names(self):
|
|
i1 = self.ticker.income_stmt
|
|
i2 = self.ticker.incomestmt
|
|
self.assertTrue(i1.equals(i2))
|
|
i3 = self.ticker.financials
|
|
self.assertTrue(i1.equals(i3))
|
|
|
|
i1 = self.ticker.get_income_stmt()
|
|
i2 = self.ticker.get_incomestmt()
|
|
self.assertTrue(i1.equals(i2))
|
|
i3 = self.ticker.get_financials()
|
|
self.assertTrue(i1.equals(i3))
|
|
|
|
i1 = self.ticker.quarterly_income_stmt
|
|
i2 = self.ticker.quarterly_incomestmt
|
|
self.assertTrue(i1.equals(i2))
|
|
i3 = self.ticker.quarterly_financials
|
|
self.assertTrue(i1.equals(i3))
|
|
|
|
i1 = self.ticker.get_income_stmt(freq="quarterly")
|
|
i2 = self.ticker.get_incomestmt(freq="quarterly")
|
|
self.assertTrue(i1.equals(i2))
|
|
i3 = self.ticker.get_financials(freq="quarterly")
|
|
self.assertTrue(i1.equals(i3))
|
|
|
|
i1 = self.ticker.ttm_income_stmt
|
|
i2 = self.ticker.ttm_incomestmt
|
|
self.assertTrue(i1.equals(i2))
|
|
i3 = self.ticker.ttm_financials
|
|
self.assertTrue(i1.equals(i3))
|
|
|
|
i1 = self.ticker.get_income_stmt(freq="trailing")
|
|
i2 = self.ticker.get_incomestmt(freq="trailing")
|
|
self.assertTrue(i1.equals(i2))
|
|
i3 = self.ticker.get_financials(freq="trailing")
|
|
self.assertTrue(i1.equals(i3))
|
|
|
|
def test_balance_sheet_alt_names(self):
|
|
i1 = self.ticker.balance_sheet
|
|
i2 = self.ticker.balancesheet
|
|
self.assertTrue(i1.equals(i2))
|
|
|
|
i1 = self.ticker.get_balance_sheet()
|
|
i2 = self.ticker.get_balancesheet()
|
|
self.assertTrue(i1.equals(i2))
|
|
|
|
i1 = self.ticker.quarterly_balance_sheet
|
|
i2 = self.ticker.quarterly_balancesheet
|
|
self.assertTrue(i1.equals(i2))
|
|
|
|
i1 = self.ticker.get_balance_sheet(freq="quarterly")
|
|
i2 = self.ticker.get_balancesheet(freq="quarterly")
|
|
self.assertTrue(i1.equals(i2))
|
|
|
|
def test_cash_flow_alt_names(self):
|
|
i1 = self.ticker.cash_flow
|
|
i2 = self.ticker.cashflow
|
|
self.assertTrue(i1.equals(i2))
|
|
|
|
i1 = self.ticker.get_cash_flow()
|
|
i2 = self.ticker.get_cashflow()
|
|
self.assertTrue(i1.equals(i2))
|
|
|
|
i1 = self.ticker.quarterly_cash_flow
|
|
i2 = self.ticker.quarterly_cashflow
|
|
self.assertTrue(i1.equals(i2))
|
|
|
|
i1 = self.ticker.get_cash_flow(freq="quarterly")
|
|
i2 = self.ticker.get_cashflow(freq="quarterly")
|
|
self.assertTrue(i1.equals(i2))
|
|
|
|
i1 = self.ticker.ttm_cash_flow
|
|
i2 = self.ticker.ttm_cashflow
|
|
self.assertTrue(i1.equals(i2))
|
|
|
|
i1 = self.ticker.get_cash_flow(freq="trailing")
|
|
i2 = self.ticker.get_cashflow(freq="trailing")
|
|
self.assertTrue(i1.equals(i2))
|
|
|
|
def test_bad_freq_value_raises_exception(self):
|
|
self.assertRaises(ValueError, lambda: self.ticker.get_cashflow(freq="badarg"))
|
|
|
|
def test_calendar(self):
|
|
data = self.ticker.calendar
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
self.assertTrue(len(data) > 0, "data is empty")
|
|
self.assertIn("Earnings Date", data.keys(), "data missing expected key")
|
|
self.assertIn("Earnings Average", data.keys(), "data missing expected key")
|
|
self.assertIn("Earnings Low", data.keys(), "data missing expected key")
|
|
self.assertIn("Earnings High", data.keys(), "data missing expected key")
|
|
self.assertIn("Revenue Average", data.keys(), "data missing expected key")
|
|
self.assertIn("Revenue Low", data.keys(), "data missing expected key")
|
|
self.assertIn("Revenue High", data.keys(), "data missing expected key")
|
|
# dividend date is not available for tested ticker GOOGL
|
|
if self.ticker.ticker != "GOOGL":
|
|
self.assertIn("Dividend Date", data.keys(), "data missing expected key")
|
|
# ex-dividend date is not always available
|
|
data_cached = self.ticker.calendar
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
# # sustainability stopped working
|
|
# def test_sustainability(self):
|
|
# data = self.ticker.sustainability
|
|
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
# self.assertFalse(data.empty, "data is empty")
|
|
|
|
# data_cached = self.ticker.sustainability
|
|
# self.assertIs(data, data_cached, "data not cached")
|
|
|
|
# def test_shares(self):
|
|
# data = self.ticker.shares
|
|
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
# self.assertFalse(data.empty, "data is empty")
|
|
|
|
|
|
class TestTickerAnalysts(unittest.TestCase):
|
|
session = None
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.session = session_gbl
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
if cls.session is not None:
|
|
cls.session.close()
|
|
|
|
def setUp(self):
|
|
self.ticker = yf.Ticker("GOOGL", session=self.session)
|
|
self.ticker_no_analysts = yf.Ticker("^GSPC", session=self.session)
|
|
|
|
def tearDown(self):
|
|
self.ticker = None
|
|
self.ticker_no_analysts = None
|
|
|
|
def test_recommendations(self):
|
|
data = self.ticker.recommendations
|
|
data_summary = self.ticker.recommendations_summary
|
|
self.assertTrue(data.equals(data_summary))
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.recommendations
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_recommendations_summary(self): # currently alias for recommendations
|
|
data = self.ticker.recommendations_summary
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.recommendations_summary
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_upgrades_downgrades(self):
|
|
data = self.ticker.upgrades_downgrades
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
self.assertIsInstance(data.index, pd.DatetimeIndex, "data has wrong index type")
|
|
|
|
data_cached = self.ticker.upgrades_downgrades
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_analyst_price_targets(self):
|
|
data = self.ticker.analyst_price_targets
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
|
|
data_cached = self.ticker.analyst_price_targets
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_earnings_estimate(self):
|
|
data = self.ticker.earnings_estimate
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.earnings_estimate
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_revenue_estimate(self):
|
|
data = self.ticker.revenue_estimate
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.revenue_estimate
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_earnings_history(self):
|
|
data = self.ticker.earnings_history
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
self.assertIsInstance(data.index, pd.DatetimeIndex, "data has wrong index type")
|
|
|
|
data_cached = self.ticker.earnings_history
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_eps_trend(self):
|
|
data = self.ticker.eps_trend
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.eps_trend
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_growth_estimates(self):
|
|
data = self.ticker.growth_estimates
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertFalse(data.empty, "data is empty")
|
|
|
|
data_cached = self.ticker.growth_estimates
|
|
self.assertIs(data, data_cached, "data not cached")
|
|
|
|
def test_no_analysts(self):
|
|
attributes = [
|
|
'recommendations',
|
|
'upgrades_downgrades',
|
|
'earnings_estimate',
|
|
'revenue_estimate',
|
|
'earnings_history',
|
|
'eps_trend',
|
|
'growth_estimates',
|
|
]
|
|
|
|
for attribute in attributes:
|
|
try:
|
|
data = getattr(self.ticker_no_analysts, attribute)
|
|
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
|
|
self.assertTrue(data.empty, "data is not empty")
|
|
except Exception as e:
|
|
self.fail(f"Exception raised for attribute '{attribute}': {e}")
|
|
|
|
|
|
|
|
class TestTickerInfo(unittest.TestCase):
|
|
session = None
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.session = session_gbl
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
if cls.session is not None:
|
|
cls.session.close()
|
|
|
|
def setUp(self):
|
|
self.symbols = []
|
|
self.symbols += ["ESLT.TA", "BP.L", "GOOGL"]
|
|
self.symbols.append("QCSTIX") # good for testing, doesn't trade
|
|
self.symbols += ["BTC-USD", "IWO", "VFINX", "^GSPC"]
|
|
self.symbols += ["SOKE.IS", "ADS.DE"] # detected bugs
|
|
self.symbols += ["EXTO" ] # Issues 2343
|
|
self.tickers = [yf.Ticker(s, session=self.session) for s in self.symbols]
|
|
|
|
def tearDown(self):
|
|
self.ticker = None
|
|
|
|
def test_fast_info(self):
|
|
f = yf.Ticker("AAPL", session=self.session).fast_info
|
|
for k in f:
|
|
self.assertIsNotNone(f[k])
|
|
|
|
def test_info(self):
|
|
data = self.tickers[0].info
|
|
self.assertIsInstance(data, dict, "data has wrong type")
|
|
expected_keys = ['industry', 'currentPrice', 'exchange', 'floatShares', 'companyOfficers', 'bid']
|
|
for k in expected_keys:
|
|
self.assertIn("symbol", data.keys(), f"Did not find expected key '{k}' in info dict")
|
|
self.assertEqual(self.symbols[0], data["symbol"], "Wrong symbol value in info dict")
|
|
|
|
def test_complementary_info(self):
|
|
# This test is to check that we can successfully retrieve the trailing PEG ratio
|
|
|
|
# We don't expect this one to have a trailing PEG ratio
|
|
data1 = self.tickers[0].info
|
|
self.assertIsNone(data1['trailingPegRatio'])
|
|
|
|
# This one should have a trailing PEG ratio
|
|
data2 = self.tickers[2].info
|
|
self.assertIsInstance(data2['trailingPegRatio'], float)
|
|
|
|
def test_isin_info(self):
|
|
isin_list = {"ES0137650018": True,
|
|
"does_not_exist": True, # Nonexistent but doesn't raise an error
|
|
"INF209K01EN2": True,
|
|
"INX846K01K35": False, # Nonexistent and raises an error
|
|
"INF846K01K35": True
|
|
}
|
|
for isin, should_succeed in isin_list.items():
|
|
if not should_succeed:
|
|
with self.assertRaises(ValueError) as context:
|
|
yf.Ticker(isin)
|
|
self.assertIn(str(context.exception), [f"Invalid ISIN number: {isin}", "Empty tickername"])
|
|
else:
|
|
ticker = yf.Ticker(isin)
|
|
try:
|
|
ticker.info # some ISINs resolve but the underlying symbol may 404
|
|
except Exception:
|
|
pass
|
|
|
|
def test_empty_info(self):
|
|
# Test issue 2343 (Empty result _fetch)
|
|
data = self.tickers[10].info
|
|
self.assertIsInstance(data, dict)
|
|
self.assertIn('quoteType', data)
|
|
self.assertIn('trailingPegRatio', data)
|
|
|
|
# def test_fast_info_matches_info(self):
|
|
# fast_info_keys = set()
|
|
# for ticker in self.tickers:
|
|
# fast_info_keys.update(set(ticker.fast_info.keys()))
|
|
# fast_info_keys = sorted(list(fast_info_keys))
|
|
|
|
# key_rename_map = {}
|
|
# key_rename_map["currency"] = "currency"
|
|
# key_rename_map["quote_type"] = "quoteType"
|
|
# key_rename_map["timezone"] = "exchangeTimezoneName"
|
|
|
|
# key_rename_map["last_price"] = ["currentPrice", "regularMarketPrice"]
|
|
# key_rename_map["open"] = ["open", "regularMarketOpen"]
|
|
# key_rename_map["day_high"] = ["dayHigh", "regularMarketDayHigh"]
|
|
# key_rename_map["day_low"] = ["dayLow", "regularMarketDayLow"]
|
|
# key_rename_map["previous_close"] = ["previousClose"]
|
|
# key_rename_map["regular_market_previous_close"] = ["regularMarketPreviousClose"]
|
|
|
|
# key_rename_map["fifty_day_average"] = "fiftyDayAverage"
|
|
# key_rename_map["two_hundred_day_average"] = "twoHundredDayAverage"
|
|
# key_rename_map["year_change"] = ["52WeekChange", "fiftyTwoWeekChange"]
|
|
# key_rename_map["year_high"] = "fiftyTwoWeekHigh"
|
|
# key_rename_map["year_low"] = "fiftyTwoWeekLow"
|
|
|
|
# key_rename_map["last_volume"] = ["volume", "regularMarketVolume"]
|
|
# key_rename_map["ten_day_average_volume"] = ["averageVolume10days", "averageDailyVolume10Day"]
|
|
# key_rename_map["three_month_average_volume"] = "averageVolume"
|
|
|
|
# key_rename_map["market_cap"] = "marketCap"
|
|
# key_rename_map["shares"] = "sharesOutstanding"
|
|
|
|
# for k in list(key_rename_map.keys()):
|
|
# if '_' in k:
|
|
# key_rename_map[yf.utils.snake_case_2_camelCase(k)] = key_rename_map[k]
|
|
|
|
# # Note: share count items in info[] are bad. Sometimes the float > outstanding!
|
|
# # So often fast_info["shares"] does not match.
|
|
# # Why isn't fast_info["shares"] wrong? Because using it to calculate market cap always correct.
|
|
# bad_keys = {"shares"}
|
|
|
|
# # Loose tolerance for averages, no idea why don't match info[]. Is info wrong?
|
|
# custom_tolerances = {}
|
|
# custom_tolerances["year_change"] = 1.0
|
|
# # custom_tolerances["ten_day_average_volume"] = 1e-3
|
|
# custom_tolerances["ten_day_average_volume"] = 1e-1
|
|
# # custom_tolerances["three_month_average_volume"] = 1e-2
|
|
# custom_tolerances["three_month_average_volume"] = 5e-1
|
|
# custom_tolerances["fifty_day_average"] = 1e-2
|
|
# custom_tolerances["two_hundred_day_average"] = 1e-2
|
|
# for k in list(custom_tolerances.keys()):
|
|
# if '_' in k:
|
|
# custom_tolerances[yf.utils.snake_case_2_camelCase(k)] = custom_tolerances[k]
|
|
|
|
# for k in fast_info_keys:
|
|
# if k in key_rename_map:
|
|
# k2 = key_rename_map[k]
|
|
# else:
|
|
# k2 = k
|
|
|
|
# if not isinstance(k2, list):
|
|
# k2 = [k2]
|
|
|
|
# for m in k2:
|
|
# for ticker in self.tickers:
|
|
# if not m in ticker.info:
|
|
# # print(f"symbol={ticker.ticker}: fast_info key '{k}' mapped to info key '{m}' but not present in info")
|
|
# continue
|
|
|
|
# if k in bad_keys:
|
|
# continue
|
|
|
|
# if k in custom_tolerances:
|
|
# rtol = custom_tolerances[k]
|
|
# else:
|
|
# rtol = 5e-3
|
|
# # rtol = 1e-4
|
|
|
|
# correct = ticker.info[m]
|
|
# test = ticker.fast_info[k]
|
|
# # print(f"Testing: symbol={ticker.ticker} m={m} k={k}: test={test} vs correct={correct}")
|
|
# if k in ["market_cap","marketCap"] and ticker.fast_info["currency"] in ["GBp", "ILA"]:
|
|
# # Adjust for currency to match Yahoo:
|
|
# test *= 0.01
|
|
# try:
|
|
# if correct is None:
|
|
# self.assertTrue(test is None or (not np.isnan(test)), f"{k}: {test} must be None or real value because correct={correct}")
|
|
# elif isinstance(test, float) or isinstance(correct, int):
|
|
# self.assertTrue(np.isclose(test, correct, rtol=rtol), f"{ticker.ticker} {k}: {test} != {correct}")
|
|
# else:
|
|
# self.assertEqual(test, correct, f"{k}: {test} != {correct}")
|
|
# except:
|
|
# if k in ["regularMarketPreviousClose"] and ticker.ticker in ["ADS.DE"]:
|
|
# # Yahoo is wrong, is returning post-market close not regular
|
|
# continue
|
|
# else:
|
|
# raise
|
|
|
|
class TestTickerFundsData(unittest.TestCase):
|
|
session = None
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.session = session_gbl
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
if cls.session is not None:
|
|
cls.session.close()
|
|
|
|
def setUp(self):
|
|
self.test_tickers = [yf.Ticker("SPY", session=self.session), # equity etf
|
|
yf.Ticker("JNK", session=self.session), # bonds etf
|
|
yf.Ticker("VTSAX", session=self.session)] # mutual fund
|
|
|
|
def tearDown(self):
|
|
self.ticker = None
|
|
|
|
def test_fetch_and_parse(self):
|
|
for ticker in self.test_tickers:
|
|
try:
|
|
ticker.funds_data._fetch_and_parse()
|
|
except Exception as e:
|
|
self.fail(f"_fetch_and_parse raised unexpected exception for {ticker.ticker}: {e}")
|
|
|
|
with self.assertRaises(YFDataException):
|
|
ticker = yf.Ticker("AAPL", session=self.session) # stock, not funds
|
|
ticker.funds_data._fetch_and_parse()
|
|
|
|
def test_description(self):
|
|
for ticker in self.test_tickers:
|
|
description = ticker.funds_data.description
|
|
self.assertIsInstance(description, str)
|
|
self.assertTrue(len(description) > 0)
|
|
|
|
def test_fund_overview(self):
|
|
for ticker in self.test_tickers:
|
|
fund_overview = ticker.funds_data.fund_overview
|
|
self.assertIsInstance(fund_overview, dict)
|
|
|
|
def test_fund_operations(self):
|
|
for ticker in self.test_tickers:
|
|
fund_operations = ticker.funds_data.fund_operations
|
|
self.assertIsInstance(fund_operations, pd.DataFrame)
|
|
|
|
def test_asset_classes(self):
|
|
for ticker in self.test_tickers:
|
|
asset_classes = ticker.funds_data.asset_classes
|
|
self.assertIsInstance(asset_classes, dict)
|
|
|
|
def test_top_holdings(self):
|
|
for ticker in self.test_tickers:
|
|
top_holdings = ticker.funds_data.top_holdings
|
|
self.assertIsInstance(top_holdings, pd.DataFrame)
|
|
|
|
def test_equity_holdings(self):
|
|
for ticker in self.test_tickers:
|
|
equity_holdings = ticker.funds_data.equity_holdings
|
|
self.assertIsInstance(equity_holdings, pd.DataFrame)
|
|
|
|
def test_bond_holdings(self):
|
|
for ticker in self.test_tickers:
|
|
bond_holdings = ticker.funds_data.bond_holdings
|
|
self.assertIsInstance(bond_holdings, pd.DataFrame)
|
|
|
|
def test_bond_ratings(self):
|
|
for ticker in self.test_tickers:
|
|
bond_ratings = ticker.funds_data.bond_ratings
|
|
self.assertIsInstance(bond_ratings, dict)
|
|
|
|
def test_sector_weightings(self):
|
|
for ticker in self.test_tickers:
|
|
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
|
|
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
|
|
self.assertIsInstance(data, pd.DataFrame)
|
|
self.assertTrue(data.empty)
|
|
|
|
def suite():
|
|
suite = unittest.TestSuite()
|
|
suite.addTest(TestTicker('Test ticker'))
|
|
suite.addTest(TestTickerEarnings('Test earnings'))
|
|
suite.addTest(TestTickerHolders('Test holders'))
|
|
suite.addTest(TestTickerHistory('Test Ticker history'))
|
|
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
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|