Skip to content

Commit 3e35ae7

Browse files
committed
Improve reading timezone from earning dates and only fall back to general symbol timezone when decoding fails
1 parent 30abd89 commit 3e35ae7

File tree

3 files changed

+47
-24
lines changed

3 files changed

+47
-24
lines changed

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ frozendict>=2.3.4
99
beautifulsoup4>=4.11.1
1010
html5lib>=1.1
1111
peewee>=3.16.2
12+
requests-cache==1.2.1
13+
requests-ratelimiter==0.7.0

tests/test_ticker.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
python -m unittest tests.ticker.TestTicker
99
1010
"""
11-
import pandas as pd
12-
13-
from .context import yfinance as yf
14-
from .context import session_gbl
15-
from yfinance.exceptions import YFChartError, YFInvalidPeriodError, YFNotImplementedError, YFTickerMissingError, YFTzMissingError
16-
1711

1812
import unittest
19-
import requests_cache
2013
from typing import Union, Any, get_args, _GenericAlias
2114
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
2215

16+
import pandas as pd
17+
import requests_cache
18+
19+
from yfinance.exceptions import YFChartError, YFInvalidPeriodError, YFNotImplementedError, YFTickerMissingError, YFTzMissingError
20+
from .context import session_gbl
21+
from .context import yfinance as yf
22+
2323
ticker_attributes = (
2424
("major_holders", pd.DataFrame),
2525
("institutional_holders", pd.DataFrame),
@@ -289,6 +289,7 @@ def test_earnings_dates(self):
289289
data = self.ticker.earnings_dates
290290
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
291291
self.assertFalse(data.empty, "data is empty")
292+
self.assertEqual(data.index.tz.zone, "America/New_York")
292293

293294
def test_earnings_dates_with_limit(self):
294295
# use ticker with lots of historic earnings
@@ -298,6 +299,7 @@ def test_earnings_dates_with_limit(self):
298299
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
299300
self.assertFalse(data.empty, "data is empty")
300301
self.assertEqual(len(data), limit, "Wrong number or rows")
302+
self.assertEqual(data.index[0].tz.zone, "America/New_York")
301303

302304
data_cached = ticker.get_earnings_dates(limit=limit)
303305
self.assertIs(data, data_cached, "data not cached")
@@ -323,6 +325,15 @@ def test_earnings_dates_with_limit(self):
323325
# data_cached = self.ticker.earnings_trend
324326
# self.assertIs(data, data_cached, "data not cached")
325327

328+
def test_ticker_has_tz(self):
329+
test_data = {"AMZN": "America/New_York", "LHA.DE": "Europe/Berlin", "6758.T": "Asia/Tokyo"}
330+
for symbol, tz in test_data.items():
331+
with self.subTest(f"{symbol}-{tz}"):
332+
ticker = yf.Ticker(symbol)
333+
data = ticker.get_earnings_dates(limit=1)
334+
self.assertIsNotNone(data.index.tz)
335+
self.assertEqual(data.index.tz.zone, tz)
336+
326337

327338
class TestTickerHolders(unittest.TestCase):
328339
session = None

yfinance/base.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,26 @@
2121

2222
from __future__ import print_function
2323

24-
from io import StringIO
2524
import json as _json
25+
import re
2626
import warnings
27+
from io import StringIO
2728
from typing import Optional, Union
2829
from urllib.parse import quote as urlencode
2930

3031
import pandas as pd
32+
import pytz
3133
import requests
3234

3335
from . import utils, cache
36+
from .const import _BASE_URL_, _ROOT_URL_
3437
from .data import YfData
3538
from .exceptions import YFEarningsDateMissing
3639
from .scrapers.analysis import Analysis
3740
from .scrapers.fundamentals import Fundamentals
41+
from .scrapers.history import PriceHistory
3842
from .scrapers.holders import Holders
3943
from .scrapers.quote import Quote, FastInfo
40-
from .scrapers.history import PriceHistory
41-
42-
from .const import _BASE_URL_, _ROOT_URL_
4344

4445

4546
class TickerBase:
@@ -534,6 +535,15 @@ def get_earnings_dates(self, limit=12, proxy=None) -> Optional[pd.DataFrame]:
534535

535536
logger = utils.get_yf_logger()
536537

538+
ticker_tz = ""
539+
540+
def get_ticker_tz():
541+
nonlocal ticker_tz
542+
if ticker_tz == "":
543+
self._quote.proxy = proxy or self.proxy
544+
ticker_tz = self._get_ticker_tz(proxy=proxy, timeout=30)
545+
return ticker_tz
546+
537547
page_size = min(limit, 100) # YF caps at 100, don't go higher
538548
page_offset = 0
539549
dates = None
@@ -589,20 +599,20 @@ def get_earnings_dates(self, limit=12, proxy=None) -> Optional[pd.DataFrame]:
589599

590600
# Parse earnings date string
591601
cn = "Earnings Date"
592-
# - remove AM/PM and timezone from date string
593-
tzinfo = dates[cn].str.extract('([AP]M[a-zA-Z]*)$')
594-
dates[cn] = dates[cn].replace(' [AP]M[a-zA-Z]*$', '', regex=True)
595-
# - split AM/PM from timezone
596-
tzinfo = tzinfo[0].str.extract('([AP]M)([a-zA-Z]*)', expand=True)
597-
tzinfo.columns = ["AM/PM", "TZ"]
598-
# - combine and parse
599-
dates[cn] = dates[cn] + ' ' + tzinfo["AM/PM"]
600-
dates[cn] = pd.to_datetime(dates[cn], format="%b %d, %Y, %I %p")
601-
# - instead of attempting decoding of ambiguous timezone abbreviation, just use 'info':
602-
self._quote.proxy = proxy or self.proxy
603-
tz = self._get_ticker_tz(proxy=proxy, timeout=30)
604-
dates[cn] = dates[cn].dt.tz_localize(tz)
605602

603+
def map_date(time_str: str):
604+
tz_match = re.search('([AP]M)([a-zA-Z]*)$', time_str)
605+
tz_str = tz_match.group(2).strip()
606+
# - remove AM/PM and timezone from date string
607+
time_str = time_str.replace(tz_str, "")
608+
try:
609+
tz = pytz.timezone(tz_str)
610+
except pytz.UnknownTimeZoneError:
611+
tz = get_ticker_tz()
612+
613+
return pd.to_datetime(time_str, format="%b %d, %Y, %I %p").tz_localize(tz)
614+
615+
dates[cn] = dates[cn].map(map_date)
606616
dates = dates.set_index("Earnings Date")
607617

608618
self._earnings_dates[limit] = dates

0 commit comments

Comments
 (0)