Skip to content

Commit d1c69e1

Browse files
authored
Merge pull request #2637 from ranaroussi/dev
sync dev -> main
2 parents 50bf706 + 6d07669 commit d1c69e1

35 files changed

+1251
-596
lines changed

doc/source/advanced/config.rst

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,55 @@
22
Config
33
******
44

5-
`yfinance` has a new global config for sharing common values.
5+
`yfinance` has a new global config for sharing common values:
66

7-
Proxy
7+
.. code-block:: python
8+
9+
>>> import yfinance as yf
10+
>>> yf.config
11+
{
12+
"network": {
13+
"proxy": null,
14+
"retries": 0
15+
},
16+
"debug": {
17+
"hide_exceptions": true,
18+
"logging": false
19+
}
20+
}
21+
>>> yf.config.network
22+
{
23+
"proxy": null,
24+
"retries": 0
25+
}
26+
27+
28+
Network
29+
-------
30+
31+
* **proxy** - Set proxy for all yfinance data fetches.
32+
33+
.. code-block:: python
34+
35+
yf.config.network.proxy = "PROXY_SERVER"
36+
37+
* **retries** - Configure automatic retry for transient network errors. The retry mechanism uses exponential backoff (1s, 2s, 4s...).
38+
39+
.. code-block:: python
40+
41+
yf.config.network.retries = 2
42+
43+
Debug
844
-----
945

10-
Set proxy once in config, affects all yfinance data fetches.
46+
* **hide_exceptions** - Set to `False` to stop yfinance hiding exceptions.
1147

12-
.. code-block:: python
48+
.. code-block:: python
49+
50+
yf.config.debug.hide_exceptions = False
51+
52+
* **logging** - Set to `True` to enable verbose debug logging.
53+
54+
.. code-block:: python
1355
14-
import yfinance as yf
15-
yf.set_config(proxy="PROXY_SERVER")
56+
yf.config.debug.logging = True
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import yfinance as yf
2+
from datetime import datetime, timedelta
3+
4+
# Default init (today + 7 days)
5+
calendar = yf.Calendars()
6+
7+
# Today's events: calendar of 1 day
8+
tomorrow = datetime.now() + timedelta(days=1)
9+
calendar = yf.Calendars(end=tomorrow)
10+
11+
# Default calendar queries - accessing the properties will fetch the data from YF
12+
calendar.earnings_calendar
13+
calendar.ipo_info_calendar
14+
calendar.splits_calendar
15+
calendar.economic_events_calendar
16+
17+
# Manual queries
18+
calendar.get_earnings_calendar()
19+
calendar.get_ipo_info_calendar()
20+
calendar.get_splits_calendar()
21+
calendar.get_economic_events_calendar()
22+
23+
# Earnings calendar custom filters
24+
calendar.get_earnings_calendar(
25+
market_cap=100_000_000, # filter out small-cap
26+
filter_most_active=True, # show only actively traded. Uses: `screen(query="MOST_ACTIVES")`
27+
)
28+
29+
# Example of real use case:
30+
# Get inminent unreported earnings events
31+
today = datetime.now()
32+
is_friday = today.weekday() == 4
33+
day_after_tomorrow = today + timedelta(days=4 if is_friday else 2)
34+
35+
calendar = yf.Calendars(today, day_after_tomorrow)
36+
df = calendar.get_earnings_calendar(limit=100)
37+
38+
unreported_df = df[df["Reported EPS"].isnull()]

doc/source/reference/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The following are the publicly available classes, and functions exposed by the `
1616
- :attr:`Ticker <yfinance.Ticker>`: Class for accessing single ticker data.
1717
- :attr:`Tickers <yfinance.Tickers>`: Class for handling multiple tickers.
1818
- :attr:`Market <yfinance.Market>`: Class for accessing market summary.
19+
- :attr:`Calendars <yfinance.Calendars>`: Class for accessing calendar events data.
1920
- :attr:`download <yfinance.download>`: Function to download market data for multiple tickers.
2021
- :attr:`Search <yfinance.Search>`: Class for accessing search results.
2122
- :attr:`Lookup <yfinance.Lookup>`: Class for looking up tickers.
@@ -37,6 +38,7 @@ The following are the publicly available classes, and functions exposed by the `
3738
yfinance.ticker_tickers
3839
yfinance.stock
3940
yfinance.market
41+
yfinance.calendars
4042
yfinance.financials
4143
yfinance.analysis
4244
yfinance.search
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
=====================
2+
Calendars
3+
=====================
4+
5+
.. currentmodule:: yfinance
6+
7+
8+
Class
9+
------------
10+
The `Calendars` class allows you to get information about upcoming events, for example, earning events.
11+
12+
.. autosummary::
13+
:toctree: api/
14+
15+
Calendars
16+
17+
Sample Code
18+
------------------
19+
20+
.. literalinclude:: examples/calendars.py
21+
:language: python

meta.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ build:
1515
script: "{{ PYTHON }} -m pip install . --no-deps --ignore-installed -vv "
1616

1717
requirements:
18+
# curl_cffi 0.14 has major problems, see their Github
1819
host:
1920
- pandas >=1.3.0
2021
- numpy >=1.16.5
@@ -26,7 +27,7 @@ requirements:
2627
- frozendict >=2.3.4
2728
- beautifulsoup4 >=4.11.1
2829
- html5lib >=1.1
29-
- curl_cffi >=0.7
30+
- curl_cffi >=0.7,<0.14
3031
- peewee >=3.16.2
3132
- pip
3233
- python
@@ -42,7 +43,7 @@ requirements:
4243
- frozendict >=2.3.4
4344
- beautifulsoup4 >=4.11.1
4445
- html5lib >=1.1
45-
- curl_cffi >=0.7
46+
- curl_cffi >=0.7,<0.14
4647
- peewee >=3.16.2
4748
- python
4849

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ peewee>=3.16.2
1010
requests_cache>=1.0
1111
requests_ratelimiter>=0.3.1
1212
scipy>=1.6.3
13-
curl_cffi>=0.7
13+
# curl_cffi 0.14 has major problems, see their Github
14+
curl_cffi>=0.7,<0.14
1415
protobuf>=3.19.0
1516
websockets>=13.0

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@
5959
platforms=['any'],
6060
keywords='pandas, yahoo finance, pandas datareader',
6161
packages=find_packages(exclude=['contrib', 'docs', 'tests', 'examples']),
62+
# curl_cffi 0.14 has major problems, see their Github
6263
install_requires=['pandas>=1.3.0', 'numpy>=1.16.5',
6364
'requests>=2.31', 'multitasking>=0.0.7',
6465
'platformdirs>=2.0.0', 'pytz>=2022.5',
6566
'frozendict>=2.3.4', 'peewee>=3.16.2',
66-
'beautifulsoup4>=4.11.1', 'curl_cffi>=0.7',
67+
'beautifulsoup4>=4.11.1', 'curl_cffi>=0.7,<0.14',
6768
'protobuf>=3.19.0', 'websockets>=13.0'],
6869
extras_require={
6970
'nospam': ['requests_cache>=1.0', 'requests_ratelimiter>=0.3.1'],

tests/test_calendars.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from datetime import datetime, timedelta, timezone
2+
import unittest
3+
4+
import pandas as pd
5+
6+
from tests.context import yfinance as yf, session_gbl
7+
8+
9+
class TestCalendars(unittest.TestCase):
10+
def setUp(self):
11+
self.calendars = yf.Calendars(session=session_gbl)
12+
13+
def test_get_earnings_calendar(self):
14+
result = self.calendars.get_earnings_calendar(limit=1)
15+
tickers = self.calendars.earnings_calendar.index.tolist()
16+
17+
self.assertIsInstance(result, pd.DataFrame)
18+
self.assertEqual(len(result), 1)
19+
self.assertIsInstance(tickers, list)
20+
self.assertEqual(len(tickers), len(result))
21+
self.assertEqual(tickers, result.index.tolist())
22+
23+
first_ticker = result.index.tolist()[0]
24+
result_first_ticker = self.calendars.earnings_calendar.loc[first_ticker].name
25+
self.assertEqual(first_ticker, result_first_ticker)
26+
27+
def test_get_earnings_calendar_init_params(self):
28+
result = self.calendars.get_earnings_calendar(limit=5)
29+
self.assertGreaterEqual(result['Event Start Date'].iloc[0], pd.to_datetime(datetime.now(tz=timezone.utc)))
30+
31+
start = datetime.now(tz=timezone.utc) - timedelta(days=7)
32+
result = yf.Calendars(start=start).get_earnings_calendar(limit=5)
33+
self.assertGreaterEqual(result['Event Start Date'].iloc[0], pd.to_datetime(start))
34+
35+
def test_get_ipo_info_calendar(self):
36+
result = self.calendars.get_ipo_info_calendar(limit=5)
37+
38+
self.assertIsInstance(result, pd.DataFrame)
39+
self.assertEqual(len(result), 5)
40+
41+
def test_get_economic_events_calendar(self):
42+
result = self.calendars.get_economic_events_calendar(limit=5)
43+
44+
self.assertIsInstance(result, pd.DataFrame)
45+
self.assertEqual(len(result), 5)
46+
47+
def test_get_splits_calendar(self):
48+
result = self.calendars.get_splits_calendar(limit=5)
49+
50+
self.assertIsInstance(result, pd.DataFrame)
51+
self.assertEqual(len(result), 5)
52+
53+
54+
if __name__ == "__main__":
55+
unittest.main()

tests/test_price_repair.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def test_resampling(self):
6161
vol_diff_pct0 = (dfr['Volume'].iloc[0] - df_truth['Volume'].iloc[0])/df_truth['Volume'].iloc[0]
6262
vol_diff_pct1 = (dfr['Volume'].iloc[-1] - df_truth['Volume'].iloc[-1])/df_truth['Volume'].iloc[-1]
6363
vol_diff_pct = _np.array([vol_diff_pct0, vol_diff_pct1])
64-
vol_match = vol_diff_pct > -0.23
64+
vol_match = vol_diff_pct > -0.32
6565
vol_match_nmatch = _np.sum(vol_match)
6666
vol_match_ndiff = len(vol_match) - vol_match_nmatch
6767
if vol_match.all():
@@ -78,6 +78,8 @@ def test_resampling(self):
7878

7979
if debug:
8080
print("- investigate:")
81+
print(f" - interval = {interval}")
82+
print(f" - period = {period}")
8183
print("- df_truth:")
8284
print(df_truth)#[['Open', 'Close', 'Volume']])
8385
df_1d = dat.history(interval='1d', period=period)
@@ -361,27 +363,23 @@ def test_repair_zeroes_daily(self):
361363
hist = dat._lazy_load_price_history()
362364
tz_exchange = dat.fast_info["timezone"]
363365

364-
df_bad = _pd.DataFrame(data={"Open": [0, 114.37, 114.20],
365-
"High": [0, 114.40, 114.40],
366-
"Low": [0, 114.36, 114.20],
367-
"Close": [114.39, 114.38, 114.45],
368-
"Adj Close": [114.39, 114.38, 114.45],
369-
"Volume": [9, 15666, 1094]},
370-
index=_pd.to_datetime([_dt.datetime(2025, 3, 17),
371-
_dt.datetime(2025, 3, 14),
372-
_dt.datetime(2025, 3, 13)]))
373-
df_bad = df_bad.sort_index()
374-
df_bad.index.name = "Date"
375-
df_bad.index = df_bad.index.tz_localize(tz_exchange)
366+
correct_df = dat.history(period='1mo', auto_adjust=False)
367+
368+
dt_bad = correct_df.index[len(correct_df)//2]
369+
df_bad = correct_df.copy()
370+
for c in df_bad.columns:
371+
df_bad.loc[dt_bad, c] = _np.nan
376372

377373
repaired_df = hist._fix_zeroes(df_bad, "1d", tz_exchange, prepost=False)
378374

379-
correct_df = df_bad.copy()
380-
correct_df.loc["2025-03-17", "Open"] = 114.62
381-
correct_df.loc["2025-03-17", "High"] = 114.62
382-
correct_df.loc["2025-03-17", "Low"] = 114.41
383375
for c in ["Open", "Low", "High", "Close"]:
384-
self.assertTrue(_np.isclose(repaired_df[c], correct_df[c], rtol=1e-7).all())
376+
try:
377+
self.assertTrue(_np.isclose(repaired_df[c], correct_df[c], rtol=1e-7).all())
378+
except Exception:
379+
print(f"# column = {c}")
380+
print("# correct:") ; print(correct_df[c])
381+
print("# repaired:") ; print(repaired_df[c])
382+
raise
385383

386384
self.assertTrue("Repaired?" in repaired_df.columns)
387385
self.assertFalse(repaired_df["Repaired?"].isna().any())
@@ -421,7 +419,13 @@ def test_repair_zeroes_daily_adjClose(self):
421419

422420
df_slice_bad_repaired = hist._fix_zeroes(df_slice_bad, "1d", tz_exchange, prepost=False)
423421
for c in ["Close", "Adj Close"]:
424-
self.assertTrue(_np.isclose(df_slice_bad_repaired[c], df_slice[c], rtol=rtol).all())
422+
try:
423+
self.assertTrue(_np.isclose(df_slice_bad_repaired[c], df_slice[c], rtol=rtol).all())
424+
except Exception:
425+
print(f"# column = {c}")
426+
print("# correct:") ; print(df_slice[c])
427+
print("# repaired:") ; print(df_slice_bad_repaired[c])
428+
raise
425429
self.assertTrue("Repaired?" in df_slice_bad_repaired.columns)
426430
self.assertFalse(df_slice_bad_repaired["Repaired?"].isna().any())
427431

@@ -464,7 +468,7 @@ def test_repair_bad_stock_splits(self):
464468
# Stocks that split in 2022 but no problems in Yahoo data,
465469
# so repair should change nothing
466470
good_tkrs = ['AMZN', 'DXCM', 'FTNT', 'GOOG', 'GME', 'PANW', 'SHOP', 'TSLA']
467-
good_tkrs += ['AEI', 'GHI', 'IRON', 'LXU', 'RSLS', 'TISI']
471+
good_tkrs += ['AEI', 'GHI', 'IRON', 'LXU', 'TISI']
468472
good_tkrs += ['BOL.ST', 'TUI1.DE']
469473
intervals = ['1d', '1wk', '1mo', '3mo']
470474
for tkr in good_tkrs:
@@ -589,7 +593,6 @@ def test_repair_bad_div_adjusts(self):
589593

590594
# Div 0.01x
591595
bad_tkrs += ['NVT.L']
592-
bad_tkrs += ['TENT.L']
593596

594597
# Missing div adjusts:
595598
bad_tkrs += ['1398.HK']

tests/test_prices.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from tests.context import session_gbl
33

44
import unittest
5+
import socket
56

67
import datetime as _dt
78
import pytz as _tz
@@ -456,6 +457,21 @@ def test_aggregate_capital_gains(self):
456457

457458
dat.history(start=start, end=end, interval=interval)
458459

460+
def test_transient_error_detection(self):
461+
"""Test that _is_transient_error correctly identifies transient vs permanent errors"""
462+
from yfinance.data import _is_transient_error
463+
from yfinance.exceptions import YFPricesMissingError
464+
465+
# Transient errors (should retry)
466+
self.assertTrue(_is_transient_error(socket.error("Network error")))
467+
self.assertTrue(_is_transient_error(TimeoutError("Timeout")))
468+
self.assertTrue(_is_transient_error(OSError("OS error")))
469+
470+
# Permanent errors (should NOT retry)
471+
self.assertFalse(_is_transient_error(ValueError("Invalid")))
472+
self.assertFalse(_is_transient_error(YFPricesMissingError('INVALID', '')))
473+
self.assertFalse(_is_transient_error(KeyError("key")))
474+
459475

460476
if __name__ == '__main__':
461477
unittest.main()

0 commit comments

Comments
 (0)