Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ pytz>=2022.5
frozendict>=2.3.4
beautifulsoup4>=4.11.1
html5lib>=1.1
peewee>=3.16.2
peewee>=3.16.2
21 changes: 14 additions & 7 deletions tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,14 @@
python -m unittest tests.ticker.TestTicker

"""
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


import unittest
import requests_cache
from typing import Union, Any, get_args, _GenericAlias
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse

import pandas as pd
import requests_cache

ticker_attributes = (
("major_holders", pd.DataFrame),
("institutional_holders", pd.DataFrame),
Expand Down Expand Up @@ -305,6 +301,7 @@ 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")
self.assertEqual(data.index.tz.zone, "America/New_York")

def test_earnings_dates_with_limit(self):
# use ticker with lots of historic earnings
Expand All @@ -314,6 +311,7 @@ def test_earnings_dates_with_limit(self):
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")
self.assertEqual(data.index[0].tz.zone, "America/New_York")

data_cached = ticker.get_earnings_dates(limit=limit)
self.assertIs(data, data_cached, "data not cached")
Expand All @@ -339,6 +337,15 @@ def test_earnings_dates_with_limit(self):
# data_cached = self.ticker.earnings_trend
# self.assertIs(data, data_cached, "data not cached")

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


class TestTickerHolders(unittest.TestCase):
session = None
Expand Down
62 changes: 54 additions & 8 deletions yfinance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,25 @@

from __future__ import print_function

from io import StringIO
import json as _json
import warnings
from io import StringIO
from typing import Optional, Union
from urllib.parse import quote as urlencode

import pandas as pd
import requests

from . import utils, cache
from .const import _BASE_URL_, _ROOT_URL_
from .data import YfData
from .exceptions import YFEarningsDateMissing
from .scrapers.analysis import Analysis
from .scrapers.fundamentals import Fundamentals
from .scrapers.funds import FundsData
from .scrapers.history import PriceHistory
from .scrapers.holders import Holders
from .scrapers.quote import Quote, FastInfo
from .scrapers.history import PriceHistory
from .scrapers.funds import FundsData

from .const import _BASE_URL_, _ROOT_URL_


class TickerBase:
Expand Down Expand Up @@ -575,6 +574,15 @@ def get_earnings_dates(self, limit=12, proxy=None) -> Optional[pd.DataFrame]:

logger = utils.get_yf_logger()

ticker_tz = ""

def get_ticker_tz():
nonlocal ticker_tz
if ticker_tz == "":
self._quote.proxy = proxy or self.proxy
ticker_tz = self._get_ticker_tz(proxy=proxy, timeout=30)
return ticker_tz

page_size = min(limit, 100) # YF caps at 100, don't go higher
page_offset = 0
dates = None
Expand Down Expand Up @@ -639,10 +647,48 @@ def get_earnings_dates(self, limit=12, proxy=None) -> Optional[pd.DataFrame]:
# - combine and parse
dates[cn] = dates[cn] + ' ' + tzinfo["AM/PM"]
dates[cn] = pd.to_datetime(dates[cn], format="%b %d, %Y, %I %p")
# - instead of attempting decoding of ambiguous timezone abbreviation, just use 'info':

# Try to remap all ambiguous timezone values:
tzinfo['TZ'] = tzinfo['TZ'].str.replace('BST', 'Europe/London')
tzinfo['TZ'] = tzinfo['TZ'].str.replace('GMT', 'Europe/London')
if '.' not in self.ticker:
tzinfo['TZ'] = tzinfo['TZ'].str.replace('EST', 'America/New_York')
elif self.ticker.endswith(".AX"):
tzinfo['TZ'] = tzinfo['TZ'].str.replace('EST', 'Australia/Sydney')
tzinfo['TZ'] = tzinfo['TZ'].str.replace('MST', 'America/Denver')
tzinfo['TZ'] = tzinfo['TZ'].str.replace('PST', 'America/Los_Angeles')
if'.' not in self.ticker:
tzinfo['TZ'] = tzinfo['TZ'].str.replace('CST', 'America/Chicago')
else:
# Revisit if Cuba get a stock exchange
tzinfo['TZ'] = tzinfo['TZ'].str.replace('CST', 'Asia/Shanghai')
if self.ticker.endswith('.TA'):
tzinfo['TZ'] = tzinfo['TZ'].str.replace('IST', 'Asia/Jerusalem')
elif self.ticker.endswith('.IR'):
tzinfo['TZ'] = tzinfo['TZ'].str.replace('IST', 'Europe/Dublin')
elif self.ticker.endswith('.NS'):
tzinfo['TZ'] = tzinfo['TZ'].str.replace('IST', 'Asia/Kolkata')

# But in case still ambiguity that pytz cannot parse, have a backup:
self._quote.proxy = proxy or self.proxy
tz = self._get_ticker_tz(proxy=proxy, timeout=30)
dates[cn] = dates[cn].dt.tz_localize(tz)
tz_backup = self._get_ticker_tz(proxy=proxy, timeout=30)

if len(tzinfo['TZ'].unique())==1:
try:
dates[cn] = dates[cn].dt.tz_localize(tzinfo['TZ'].iloc[0])
except Exception:
dates[cn] = dates[cn].dt.tz_localize(tz_backup)
else:
dates2 = []
for i in range(len(dates)):
dt = dates[cn].iloc[i]
tz = tzinfo['TZ'].iloc[i]
try:
dt = dt.tz_localize(tz)
except Exception:
dt = dt.tz_localize(tz_backup)
dates2.append(dt)
dates[cn] = pd.to_datetime(dates2, utc=True).tz_convert(dates2[0].tzinfo)

dates = dates.set_index("Earnings Date")

Expand Down