Skip to content

Commit

Permalink
Fix custom periods
Browse files Browse the repository at this point in the history
  • Loading branch information
dhruvan2006 committed Jan 7, 2025
1 parent 2919ccb commit 2d60f9a
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 11 deletions.
51 changes: 50 additions & 1 deletion tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
python -m unittest tests.ticker.TestTicker
"""
from datetime import datetime, timedelta

import pandas as pd

from tests.context import yfinance as yf
Expand Down Expand Up @@ -133,8 +135,55 @@ def test_invalid_period(self):
with self.assertRaises(YFInvalidPeriodError):
dat.history(period="2wks", interval="1d", raise_errors=True)
with self.assertRaises(YFInvalidPeriodError):
dat.history(period="2mo", interval="1d", raise_errors=True)
dat.history(period="2mos", interval="1d", raise_errors=True)

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)

for period, interval in valid_periods:
with self.subTest(period=period, interval=interval):
df = dat.history(period=period, interval=interval, raise_errors=True)
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=7),
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}")

def test_prices_missing(self):
# this test will need to be updated every time someone wants to run a test
Expand Down
3 changes: 2 additions & 1 deletion yfinance/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ def __init__(self, ticker, invalid_period, valid_ranges):
self.ticker = ticker
self.invalid_period = invalid_period
self.valid_ranges = valid_ranges
super().__init__(f"{self.ticker}: Period '{invalid_period}' is invalid, must be one of {valid_ranges}")
super().__init__(f"{self.ticker}: Period '{invalid_period}' is invalid, "
f"must be of the format {valid_ranges}, etc.")


class YFRateLimitError(YFException):
Expand Down
19 changes: 10 additions & 9 deletions yfinance/scrapers/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,6 @@ def history(self, period="1mo", interval="1d",
period_user = period
period = None
interval = '1d'
else:
end = utils._parse_user_dt(end, self.tz)
start = _datetime.date.fromordinal(end)
start -= utils._interval_to_timedelta(period)
start -= _datetime.timedelta(days=4)

start_user = start
end_user = end
Expand Down Expand Up @@ -237,10 +232,9 @@ def history(self, period="1mo", interval="1d",
elif "chart" not in data or data["chart"]["result"] is None or not data["chart"]["result"] or not data["chart"]["result"][0]["indicators"]["quote"][0]:
_exception = YFPricesMissingError(self.ticker, _price_data_debug)
fail = True
elif period is not None and period not in self._history_metadata["validRanges"]:
# even if timestamp is in the data, the data doesn't encompass the period requested
# User provided a bad period. The minimum should be '1d', but sometimes Yahoo accepts '1h'.
_exception = YFInvalidPeriodError(self.ticker, period, self._history_metadata['validRanges'])
elif period and period not in self._history_metadata['validRanges'] and not utils.is_valid_period_format(period):
# User provided a bad period
_exception = YFInvalidPeriodError(self.ticker, period, ", ".join(self._history_metadata['validRanges']))
fail = True

if fail:
Expand All @@ -255,6 +249,13 @@ def history(self, period="1mo", interval="1d",
self._reconstruct_start_interval = None
return utils.empty_df()

# Process custom periods
if period and period not in self._history_metadata.get("validRanges", []):
end = int(_time.time())
start = _datetime.date.fromtimestamp(end)
start -= utils._interval_to_timedelta(period)
start -= _datetime.timedelta(days=4)

# parse quotes
quotes = utils.parse_quotes(data["chart"]["result"][0])
# Yahoo bug fix - it often appends latest price even if after end date
Expand Down

0 comments on commit 2d60f9a

Please sign in to comment.