From f13ff4bb4c35f6171722982b74a971f0b9c614b3 Mon Sep 17 00:00:00 2001 From: Mike Reiche Date: Wed, 7 Aug 2024 20:24:15 +0200 Subject: [PATCH 1/6] Fix datetime conversion with mixed timezones when ignore_tz is False --- tests/test_prices.py | 30 ++++++++++++++++++------------ tests/test_utils.py | 27 ++++++++++++++++++++++++++- yfinance/multi.py | 2 +- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/tests/test_prices.py b/tests/test_prices.py index 0e9ba68c9..29ae34c89 100644 --- a/tests/test_prices.py +++ b/tests/test_prices.py @@ -1,13 +1,13 @@ -from .context import yfinance as yf -from .context import session_gbl - +import datetime as _dt +import os import unittest -import os -import datetime as _dt -import pytz as _tz import numpy as _np import pandas as _pd +import pytz as _tz + +from .context import session_gbl +from .context import yfinance as yf class TestPriceHistory(unittest.TestCase): @@ -32,17 +32,23 @@ def test_daily_index(self): f = df.index.time == _dt.time(0) self.assertTrue(f.all()) - def test_download(self): + def test_download_multi_large_interval(self): tkrs = ["BHP.AX", "IMP.JO", "BP.L", "PNL.L", "INTC"] intervals = ["1d", "1wk", "1mo"] for interval in intervals: - df = yf.download(tkrs, period="5y", interval=interval) + with self.subTest(interval): + df = yf.download(tkrs, period="5y", interval=interval) + + f = df.index.time == _dt.time(0) + self.assertTrue(f.all()) - f = df.index.time == _dt.time(0) - self.assertTrue(f.all()) + df_tkrs = df.columns.levels[1] + self.assertEqual(sorted(tkrs), sorted(df_tkrs)) - df_tkrs = df.columns.levels[1] - self.assertEqual(sorted(tkrs), sorted(df_tkrs)) + def test_download_multi_small_interval(self): + use_tkrs = ["AAPL", "0Q3.DE", "ATVI"] + df = yf.download(use_tkrs, period="1d", interval="5m") + self.assertEqual(df.index.tz, _dt.timezone.utc) def test_download_with_invalid_ticker(self): #Checks if using an invalid symbol gives the same output as not using an invalid symbol in combination with a valid symbol (AAPL) diff --git a/tests/test_utils.py b/tests/test_utils.py index 48bb5edad..e3f66e592 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,9 +8,10 @@ python -m unittest tests.utils.TestTicker """ +from datetime import datetime from unittest import TestSuite -# import pandas as pd +import pandas as pd # import numpy as np from .context import yfinance as yf @@ -81,10 +82,34 @@ def test_tzCacheRootLookup(self): cache.lookup(tkr) +class TestPandas(unittest.TestCase): + date_strings = ["2024-08-07 09:05:00+02:00", "2024-08-07 09:05:00-04:00"] + + @unittest.expectedFailure + def test_mixed_timezones_to_datetime_fails(self): + series = pd.Series(self.date_strings) + series = series.map(pd.Timestamp) + converted = pd.to_datetime(series) + self.assertIsNotNone(converted[0].tz) + + def test_mixed_timezones_to_datetime(self): + series = pd.Series(self.date_strings) + series = series.map(pd.Timestamp) + converted = pd.to_datetime(series, utc=True) + self.assertIsNotNone(converted[0].tz) + i = 0 + for dt in converted: + dt: datetime + ts: pd.Timestamp = series[i] + self.assertEqual(dt.isoformat(), ts.tz_convert(tz="UTC").isoformat()) + i += 1 + + def suite(): ts: TestSuite = unittest.TestSuite() ts.addTest(TestCache('Test cache')) ts.addTest(TestCacheNoPermission('Test cache no permission')) + ts.addTest(TestPandas("Test pandas")) return ts diff --git a/yfinance/multi.py b/yfinance/multi.py index cee8e923f..e42add916 100644 --- a/yfinance/multi.py +++ b/yfinance/multi.py @@ -211,7 +211,7 @@ def download(tickers, start=None, end=None, actions=False, threads=True, ignore_ _realign_dfs() data = _pd.concat(shared._DFS.values(), axis=1, sort=True, keys=shared._DFS.keys(), names=['Ticker', 'Price']) - data.index = _pd.to_datetime(data.index) + data.index = _pd.to_datetime(data.index, utc=True) # switch names back to isins if applicable data.rename(columns=shared._ISINS, inplace=True) From 8b8db167f5e61e36a5ba6b4c1bee2bd3e058aeb6 Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Mon, 19 Aug 2024 10:10:57 +0100 Subject: [PATCH 2/6] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index e27da413f..dd7b6fcfc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -55,7 +55,7 @@ body: id: debug-log attributes: label: "Debug log" - description: "Run code with debug logging enabled and post the full output. IMPORTANT INSTRUCTIONS: https://github.com/ranaroussi/yfinance/tree/main#logging" + description: "Run code with debug logging enabled - `yf.enable_debug_mode()` - and post the full output. Context: https://github.com/ranaroussi/yfinance/tree/main#logging" validations: required: true From f67f747591ab3201c4679e60d81ef8cf5becf5e0 Mon Sep 17 00:00:00 2001 From: Aleks Fasting Date: Mon, 19 Aug 2024 20:36:12 +0200 Subject: [PATCH 3/6] add try/except for keyerror --- yfinance/scrapers/history.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/yfinance/scrapers/history.py b/yfinance/scrapers/history.py index 12cf88e77..592f095a5 100644 --- a/yfinance/scrapers/history.py +++ b/yfinance/scrapers/history.py @@ -227,8 +227,13 @@ def history(self, period="1mo", interval="1d", # 2) fix weired bug with Yahoo! - returning 60m for 30m bars if interval.lower() == "30m": logger.debug(f'{self.ticker}: resampling 30m OHLC from 15m') - exchangeStartTime = pd.Timestamp(self._history_metadata["tradingPeriods"][0][0]["start"], unit='s') - offset = str(exchangeStartTime.minute % 30)+"min" + try: + exchangeStartTime = pd.Timestamp(self._history_metadata["tradingPeriods"][0][0]["start"], unit='s') + offset = str(exchangeStartTime.minute % 30)+"min" + except KeyError: + offset = '15min' + except IndexError: + offset = '15min' quotes2 = quotes.resample('30min', offset=offset) quotes = pd.DataFrame(index=quotes2.last().index, data={ 'Open': quotes2['Open'].first(), From 683bb0dd07eb5ddbd327d7ca053f8262727b9dcc Mon Sep 17 00:00:00 2001 From: Aleks Fasting Date: Tue, 20 Aug 2024 02:57:14 +0200 Subject: [PATCH 4/6] changed metadata that time is fetched from --- yfinance/scrapers/history.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/yfinance/scrapers/history.py b/yfinance/scrapers/history.py index 592f095a5..8e211ee11 100644 --- a/yfinance/scrapers/history.py +++ b/yfinance/scrapers/history.py @@ -227,13 +227,8 @@ def history(self, period="1mo", interval="1d", # 2) fix weired bug with Yahoo! - returning 60m for 30m bars if interval.lower() == "30m": logger.debug(f'{self.ticker}: resampling 30m OHLC from 15m') - try: - exchangeStartTime = pd.Timestamp(self._history_metadata["tradingPeriods"][0][0]["start"], unit='s') - offset = str(exchangeStartTime.minute % 30)+"min" - except KeyError: - offset = '15min' - except IndexError: - offset = '15min' + exchangeStartTime = pd.Timestamp(self._history_metadata["currentTradingPeriod"]['regular']["start"], unit='s') + offset = str(exchangeStartTime.minute % 30)+"min" quotes2 = quotes.resample('30min', offset=offset) quotes = pd.DataFrame(index=quotes2.last().index, data={ 'Open': quotes2['Open'].first(), From eca141fddac89914c53d9c84e82136d47b696f53 Mon Sep 17 00:00:00 2001 From: Aleks Fasting Date: Thu, 29 Aug 2024 02:27:31 +0200 Subject: [PATCH 5/6] added unittests for prepost true and false --- tests/test_ticker.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_ticker.py b/tests/test_ticker.py index 6a04661fa..648bd14c5 100644 --- a/tests/test_ticker.py +++ b/tests/test_ticker.py @@ -12,7 +12,7 @@ from .context import yfinance as yf from .context import session_gbl -from yfinance.exceptions import YFChartError, YFInvalidPeriodError, YFNotImplementedError, YFTickerMissingError, YFTzMissingError +from yfinance.exceptions import YFInvalidPeriodError, YFNotImplementedError, YFTickerMissingError, YFTzMissingError import unittest @@ -850,7 +850,26 @@ def test_no_analysts(self): except Exception as e: self.fail(f"Excpetion raised for attribute '{attribute}': {e}") +class Test30MinResamplePreposts(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("0001.HK", session=self.session) + + def test_resample_with_prepost(self): + self.ticker.history(period='1d', interval='30m', prepost=True) + def test_resample_without_prepost(self): + self.ticker.history(period='1d', interval='30m', prepost=False) class TestTickerInfo(unittest.TestCase): session = None From b141136f086d760fd9f043fde2c9c13634e814cf Mon Sep 17 00:00:00 2001 From: Aleks Fasting Date: Thu, 29 Aug 2024 02:30:21 +0200 Subject: [PATCH 6/6] fix mistake in imports --- tests/test_ticker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_ticker.py b/tests/test_ticker.py index 648bd14c5..c096b6aa6 100644 --- a/tests/test_ticker.py +++ b/tests/test_ticker.py @@ -12,7 +12,7 @@ from .context import yfinance as yf from .context import session_gbl -from yfinance.exceptions import YFInvalidPeriodError, YFNotImplementedError, YFTickerMissingError, YFTzMissingError +from yfinance.exceptions import YFChartError, YFInvalidPeriodError, YFNotImplementedError, YFTickerMissingError, YFTzMissingError import unittest @@ -871,6 +871,7 @@ def test_resample_with_prepost(self): def test_resample_without_prepost(self): self.ticker.history(period='1d', interval='30m', prepost=False) + class TestTickerInfo(unittest.TestCase): session = None