Repository: ranaroussi/yfinance (language: python)

Issue:
Price repair bug-fix
Sort direction of prices was being flipped **during** price repair, allowing for data corruption when restoring original data where repair failed.
logging is disabled app-wide during fetching in quote.py
### Describe bug

Logging is disabled in the entire application while data fetching is happening in quote.py.

Here's the code responsible: https://github.com/ranaroussi/yfinance/blob/main/yfinance/scrapers/quote.py#L186
This change was introduced in 4e7b2094d, "Logging: improve appearance, fix propagation"

Perhaps it is possible to silence logging output only for certain loggers instead of all loggers at once?

### Simple code that reproduces your problem

```python
import logging
logging.basicConfig(level=logging.WARN)
logging.getLogger("modules").setLevel(logging.INFO)

import yfinance
def load_data():
    yfinance.Ticker("^HSI").fast_info.last_price
from threading import Thread
Thread(target=load_data, args=[], daemon=True).start()

logger = logging.getLogger("modules.Test")
logger.warning("TEST")

print("done")
```

Exprected output:
```
WARNING:modules.Test:TEST
done
```

Actual output:
```
done
```

### Debug log

done

### Bad data proof

_No response_

### `yfinance` version

main branch, commit f563e51

### Python version

3.11.6

### Operating system

_No response_


Discussion / hints:

I remember wanting to silence everything during `history` calls in `fast_info`, particularly `requests` module. But I forget why. Given `fast_info` seems stable now, probably OK to revert.
I've tested without `logging.disable`, and the logs look okay - all at DEBUG level and mostly from `yfinance` itself.

Should I make a PR?

```
DEBUG    Entering history()
DEBUG:yfinance:Entering history()
DEBUG:peewee:('CREATE TABLE IF NOT EXISTS "_kv" ("key" VARCHAR(255) NOT NULL PRIMARY KEY, "value" VARCHAR(255)) WITHOUT ROWID', [])
DEBUG:peewee:('SELECT "t1"."key", "t1"."value" FROM "_kv" AS "t1" WHERE ("t1"."key" = ?) LIMIT ? OFFSET ?', ['^HSI', 1, 0])
DEBUG     ^HSI: Yahoo GET parameters: {'range': '380d', 'interval': '1d', 'includePrePost': False, 'events': 'div,splits,capitalGains'}
DEBUG:yfinance: ^HSI: Yahoo GET parameters: {'range': '380d', 'interval': '1d', 'includePrePost': False, 'events': 'div,splits,capitalGains'}
DEBUG     Entering get()
DEBUG:yfinance: Entering get()
DEBUG      url=https://query2.finance.yahoo.com/v8/finance/chart/^HSI
DEBUG:yfinance:  url=https://query2.finance.yahoo.com/v8/finance/chart/^HSI
DEBUG      params={'range': '380d', 'interval': '1d', 'includePrePost': False, 'events': 'div,splits,capitalGains'}
DEBUG:yfinance:  params={'range': '380d', 'interval': '1d', 'includePrePost': False, 'events': 'div,splits,capitalGains'}
DEBUG      Entering _get_cookie_and_crumb()
DEBUG:yfinance:  Entering _get_cookie_and_crumb()
DEBUG       cookie_mode = 'basic'
DEBUG:yfinance:   cookie_mode = 'basic'
DEBUG       Entering _get_cookie_and_crumb_basic()
DEBUG:yfinance:   Entering _get_cookie_and_crumb_basic()
DEBUG:peewee:('CREATE TABLE IF NOT EXISTS "_cookieschema" ("strategy" VARCHAR(255) NOT NULL PRIMARY KEY, "fetch_date" DATETIME NOT NULL, "cookie_bytes" BLOB NOT NULL) WITHOUT ROWID', [])
DEBUG:peewee:('SELECT "t1"."strategy", "t1"."fetch_date", "t1"."cookie_bytes" FROM "_cookieschema" AS "t1" WHERE ("t1"."strategy" = ?) LIMIT ? OFFSET ?', ['basic', 1, 0])
DEBUG        loaded persistent cookie
DEBUG:yfinance:    loaded persistent cookie
DEBUG        reusing cookie
DEBUG:yfinance:    reusing cookie
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): query1.finance.yahoo.com:443
DEBUG:urllib3.connectionpool:https://query1.finance.yahoo.com:443 "GET /v1/test/getcrumb HTTP/1.1" 200 None
DEBUG        crumb = '3Pb9PwERTE9'
DEBUG:yfinance:    crumb = '3Pb9PwERTE9'
DEBUG       Exiting _get_cookie_and_crumb_basic()
DEBUG:yfinance:   Exiting _get_cookie_and_crumb_basic()
DEBUG      Exiting _get_cookie_and_crumb()
DEBUG:yfinance:  Exiting _get_cookie_and_crumb()
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): query2.finance.yahoo.com:443
DEBUG:urllib3.connectionpool:https://query2.finance.yahoo.com:443 "GET /v8/finance/chart/%5EHSI?range=380d&interval=1d&includePrePost=False&events=div%2Csplits%2CcapitalGains&crumb=3Pb9PwERTE9 HTTP/1.1" 200 None
DEBUG      response code=200
DEBUG:yfinance:  response code=200
DEBUG     Exiting get()
DEBUG:yfinance: Exiting get()
DEBUG     ^HSI: yfinance received OHLC data: 2022-07-04 01:30:00 -> 2024-01-16 03:30:56
DEBUG:yfinance: ^HSI: yfinance received OHLC data: 2022-07-04 01:30:00 -> 2024-01-16 03:30:56
DEBUG     ^HSI: OHLC after cleaning: 2022-07-04 09:30:00+08:00 -> 2024-01-16 11:30:56+08:00
DEBUG:yfinance: ^HSI: OHLC after cleaning: 2022-07-04 09:30:00+08:00 -> 2024-01-16 11:30:56+08:00
DEBUG     ^HSI: OHLC after combining events: 2022-07-04 00:00:00+08:00 -> 2024-01-16 00:00:00+08:00
DEBUG:yfinance: ^HSI: OHLC after combining events: 2022-07-04 00:00:00+08:00 -> 2024-01-16 00:00:00+08:00
DEBUG     ^HSI: yfinance returning OHLC: 2022-07-04 00:00:00+08:00 -> 2024-01-16 00:00:00+08:00
DEBUG:yfinance: ^HSI: yfinance returning OHLC: 2022-07-04 00:00:00+08:00 -> 2024-01-16 00:00:00+08:00
DEBUG    Exiting history()
DEBUG:yfinance:Exiting history()
```
Can do

Fix patch:
diff --git a/yfinance/base.py b/yfinance/base.py
index 573ea49f9..d395fb107 100644
--- a/yfinance/base.py
+++ b/yfinance/base.py
@@ -21,31 +21,24 @@
 
 from __future__ import print_function
 
-import datetime as _datetime
 from io import StringIO
 import json as _json
-import logging
-import time as _time
 import warnings
 from typing import Optional, Union
 from urllib.parse import quote as urlencode
 
-import dateutil as _dateutil
-import numpy as np
 import pandas as pd
 import requests
 
-from . import shared, utils, cache
+from . import utils, cache
 from .data import YfData
 from .scrapers.analysis import Analysis
 from .scrapers.fundamentals import Fundamentals
 from .scrapers.holders import Holders
 from .scrapers.quote import Quote, FastInfo
+from .scrapers.history import PriceHistory
 
-from .const import _BASE_URL_, _ROOT_URL_, price_colnames
-
-
-_empty_series = pd.Series()
+from .const import _BASE_URL_, _ROOT_URL_
 
 
 class TickerBase:
@@ -53,10 +46,6 @@ def __init__(self, ticker, session=None, proxy=None):
         self.ticker = ticker.upper()
         self.proxy = proxy
         self.session = session
-        self._history = None
-        self._history_metadata = None
-        self._history_metadata_formatted = False
-        self._base_url = _BASE_URL_
         self._tz = None
 
         self._isin = None
@@ -74,6 +63,8 @@ def __init__(self, ticker, session=None, proxy=None):
 
         self._data: YfData = YfData(session=session)
 
+        # self._price_history = PriceHistory(self._data, self.ticker)
+        self._price_history = None  # lazy-load
         self._analysis = Analysis(self._data, self.ticker)
         self._holders = Holders(self._data, self.ticker)
         self._quote = Quote(self._data, self.ticker)
@@ -81,1569 +72,16 @@ def __init__(self, ticker, session=None, proxy=None):
 
         self._fast_info = None
 
-        # Limit recursion depth when repairing prices
-        self._reconstruct_start_interval = None
-
     @utils.log_indent_decorator
-    def history(self, period="1mo", interval="1d",
-                start=None, end=None, prepost=False, actions=True,
-                auto_adjust=True, back_adjust=False, repair=False, keepna=False,
-                proxy=None, rounding=False, timeout=10,
-                raise_errors=False) -> pd.DataFrame:
-        """
-        :Parameters:
-            period : str
-                Valid periods: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max
-                Either Use period parameter or use start and end
-            interval : str
-                Valid intervals: 1m,2m,5m,15m,30m,60m,90m,1h,1d,5d,1wk,1mo,3mo
-                Intraday data cannot extend last 60 days
-            start: str
-                Download start date string (YYYY-MM-DD) or _datetime, inclusive.
-                Default is 99 years ago
-                E.g. for start="2020-01-01", the first data point will be on "2020-01-01"
-            end: str
-                Download end date string (YYYY-MM-DD) or _datetime, exclusive.
-                Default is now
-                E.g. for end="2023-01-01", the last data point will be on "2022-12-31"
-            prepost : bool
-                Include Pre and Post market data in results?
-                Default is False
-            auto_adjust: bool
-                Adjust all OHLC automatically? Default is True
-            back_adjust: bool
-                Back-adjusted data to mimic true historical prices
-            repair: bool
-                Detect currency unit 100x mixups and attempt repair.
-                Default is False
-            keepna: bool
-                Keep NaN rows returned by Yahoo?
-                Default is False
-            proxy: str
-                Optional. Proxy server URL scheme. Default is None
-            rounding: bool
-                Round values to 2 decimal places?
-                Optional. Default is False = precision suggested by Yahoo!
-            timeout: None or float
-                If not None stops waiting for a response after given number of
-                seconds. (Can also be a fraction of a second e.g. 0.01)
-                Default is 10 seconds.
-            raise_errors: bool
-                If True, then raise errors as Exceptions instead of logging.
-        """
-        logger = utils.get_yf_logger()
-        proxy = proxy or self.proxy
-
-        start_user = start
-        end_user = end
-        if start or period is None or period.lower() == "max":
-            # Check can get TZ. Fail => probably delisted
-            tz = self._get_ticker_tz(proxy, timeout)
-            if tz is None:
-                # Every valid ticker has a timezone. Missing = problem
-                err_msg = "No timezone found, symbol may be delisted"
-                shared._DFS[self.ticker] = utils.empty_df()
-                shared._ERRORS[self.ticker] = err_msg
-                if raise_errors:
-                    raise Exception(f'{self.ticker}: {err_msg}')
-                else:
-                    logger.error(f'{self.ticker}: {err_msg}')
-                return utils.empty_df()
-
-            if end is None:
-                end = int(_time.time())
-            else:
-                end = utils._parse_user_dt(end, tz)
-            if start is None:
-                if interval == "1m":
-                    start = end - 604800  # Subtract 7 days
-                else:
-                    max_start_datetime = pd.Timestamp.utcnow().floor("D") - _datetime.timedelta(days=99 * 365)
-                    start = int(max_start_datetime.timestamp())
-            else:
-                start = utils._parse_user_dt(start, tz)
-            params = {"period1": start, "period2": end}
-        else:
-            period = period.lower()
-            params = {"range": period}
-
-        params["interval"] = interval.lower()
-        params["includePrePost"] = prepost
-
-        # 1) fix weired bug with Yahoo! - returning 60m for 30m bars
-        if params["interval"] == "30m":
-            params["interval"] = "15m"
-
-        # if the ticker is MUTUALFUND or ETF, then get capitalGains events
-        params["events"] = "div,splits,capitalGains"
-
-        params_pretty = dict(params)
-        tz = self._get_ticker_tz(proxy, timeout)
-        for k in ["period1", "period2"]:
-            if k in params_pretty:
-                params_pretty[k] = str(pd.Timestamp(params[k], unit='s').tz_localize("UTC").tz_convert(tz))
-        logger.debug(f'{self.ticker}: Yahoo GET parameters: {str(params_pretty)}')
-
-        # Getting data from json
-        url = f"{self._base_url}/v8/finance/chart/{self.ticker}"
-        data = None
-        get_fn = self._data.get
-        if end is not None:
-            end_dt = pd.Timestamp(end, unit='s').tz_localize("UTC")
-            dt_now = pd.Timestamp.utcnow()
-            data_delay = _datetime.timedelta(minutes=30)
-            if end_dt + data_delay <= dt_now:
-                # Date range in past so safe to fetch through cache:
-                get_fn = self._data.cache_get
-        try:
-            data = get_fn(
-                url=url,
-                params=params,
-                proxy=proxy,
-                timeout=timeout
-            )
-            if "Will be right back" in data.text or data is None:
-                raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
-                                   "Our engineers are working quickly to resolve "
-                                   "the issue. Thank you for your patience.")
-
-            data = data.json()
-        except Exception:
-            if raise_errors:
-                raise
-
-        # Store the meta data that gets retrieved simultaneously
-        try:
-            self._history_metadata = data["chart"]["result"][0]["meta"]
-        except Exception:
-            self._history_metadata = {}
-
-        intraday = params["interval"][-1] in ("m", 'h')
-        err_msg = "No price data found, symbol may be delisted"
-        if start or period is None or period.lower() == "max":
-            err_msg += f' ({params["interval"]} '
-            if start_user is not None:
-                err_msg += f'{start_user}'
-            elif not intraday:
-                err_msg += f'{pd.Timestamp(start, unit="s").tz_localize("UTC").tz_convert(tz).date()}'
-            else:
-                err_msg += f'{pd.Timestamp(start, unit="s").tz_localize("UTC").tz_convert(tz)}'
-            err_msg += ' -> '
-            if end_user is not None:
-                err_msg += f'{end_user})'
-            elif not intraday:
-                err_msg += f'{pd.Timestamp(end, unit="s").tz_localize("UTC").tz_convert(tz).date()})'
-            else:
-                err_msg += f'{pd.Timestamp(end, unit="s").tz_localize("UTC").tz_convert(tz)})'
-        else:
-            err_msg += f' (period={period})'
-
-        fail = False
-        if data is None or not isinstance(data, dict):
-            fail = True
-        elif isinstance(data, dict) and 'status_code' in data:
-            err_msg += f"(Yahoo status_code = {data['status_code']})"
-            fail = True
-        elif "chart" in data and data["chart"]["error"]:
-            err_msg = data["chart"]["error"]["description"]
-            fail = True
-        elif "chart" not in data or data["chart"]["result"] is None or not data["chart"]["result"]:
-            fail = True
-        elif period is not None and "timestamp" not in data["chart"]["result"][0] and period not in \
-                self._history_metadata["validRanges"]:
-            # User provided a bad period. The minimum should be '1d', but sometimes Yahoo accepts '1h'.
-            err_msg = f"Period '{period}' is invalid, must be one of {self._history_metadata['validRanges']}"
-            fail = True
-        if fail:
-            shared._DFS[self.ticker] = utils.empty_df()
-            shared._ERRORS[self.ticker] = err_msg
-            if raise_errors:
-                raise Exception(f'{self.ticker}: {err_msg}')
-            else:
-                logger.error(f'{self.ticker}: {err_msg}')
-            if self._reconstruct_start_interval is not None and self._reconstruct_start_interval == interval:
-                self._reconstruct_start_interval = None
-            return utils.empty_df()
-
-        # parse quotes
-        try:
-            quotes = utils.parse_quotes(data["chart"]["result"][0])
-            # Yahoo bug fix - it often appends latest price even if after end date
-            if end and not quotes.empty:
-                endDt = pd.to_datetime(_datetime.datetime.utcfromtimestamp(end))
-                if quotes.index[quotes.shape[0] - 1] >= endDt:
-                    quotes = quotes.iloc[0:quotes.shape[0] - 1]
-        except Exception:
-            shared._DFS[self.ticker] = utils.empty_df()
-            shared._ERRORS[self.ticker] = err_msg
-            if raise_errors:
-                raise Exception(f'{self.ticker}: {err_msg}')
-            else:
-                logger.error(f'{self.ticker}: {err_msg}')
-            if self._reconstruct_start_interval is not None and self._reconstruct_start_interval == interval:
-                self._reconstruct_start_interval = None
-            return shared._DFS[self.ticker]
-        logger.debug(f'{self.ticker}: yfinance received OHLC data: {quotes.index[0]} -> {quotes.index[-1]}')
-
-        # 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')
-            quotes2 = quotes.resample('30T')
-            quotes = pd.DataFrame(index=quotes2.last().index, data={
-                'Open': quotes2['Open'].first(),
-                'High': quotes2['High'].max(),
-                'Low': quotes2['Low'].min(),
-                'Close': quotes2['Close'].last(),
-                'Adj Close': quotes2['Adj Close'].last(),
-                'Volume': quotes2['Volume'].sum()
-            })
-            try:
-                quotes['Dividends'] = quotes2['Dividends'].max()
-                quotes['Stock Splits'] = quotes2['Stock Splits'].max()
-            except Exception:
-                pass
-
-        # Select useful info from metadata
-        quote_type = self._history_metadata["instrumentType"]
-        expect_capital_gains = quote_type in ('MUTUALFUND', 'ETF')
-        tz_exchange = self._history_metadata["exchangeTimezoneName"]
-
-        # Note: ordering is important. If you change order, run the tests!
-        quotes = utils.set_df_tz(quotes, params["interval"], tz_exchange)
-        quotes = utils.fix_Yahoo_dst_issue(quotes, params["interval"])
-        quotes = utils.fix_Yahoo_returning_live_separate(quotes, params["interval"], tz_exchange)
-        intraday = params["interval"][-1] in ("m", 'h')
-        if not prepost and intraday and "tradingPeriods" in self._history_metadata:
-            tps = self._history_metadata["tradingPeriods"]
-            if not isinstance(tps, pd.DataFrame):
-                self._history_metadata = utils.format_history_metadata(self._history_metadata, tradingPeriodsOnly=True)
-                tps = self._history_metadata["tradingPeriods"]
-            quotes = utils.fix_Yahoo_returning_prepost_unrequested(quotes, params["interval"], tps)
-        logger.debug(f'{self.ticker}: OHLC after cleaning: {quotes.index[0]} -> {quotes.index[-1]}')
-
-        # actions
-        dividends, splits, capital_gains = utils.parse_actions(data["chart"]["result"][0])
-        if not expect_capital_gains:
-            capital_gains = None
-
-        if splits is not None:
-            splits = utils.set_df_tz(splits, interval, tz_exchange)
-        if dividends is not None:
-            dividends = utils.set_df_tz(dividends, interval, tz_exchange)
-        if capital_gains is not None:
-            capital_gains = utils.set_df_tz(capital_gains, interval, tz_exchange)
-        if start is not None:
-            if not quotes.empty:
-                startDt = quotes.index[0].floor('D')
-                if dividends is not None:
-                    dividends = dividends.loc[startDt:]
-                if capital_gains is not None:
-                    capital_gains = capital_gains.loc[startDt:]
-                if splits is not None:
-                    splits = splits.loc[startDt:]
-        if end is not None:
-            endDt = pd.Timestamp(end, unit='s').tz_localize(tz)
-            if dividends is not None:
-                dividends = dividends[dividends.index < endDt]
-            if capital_gains is not None:
-                capital_gains = capital_gains[capital_gains.index < endDt]
-            if splits is not None:
-                splits = splits[splits.index < endDt]
-
-        # Prepare for combine
-        intraday = params["interval"][-1] in ("m", 'h')
-        if not intraday:
-            # If localizing a midnight during DST transition hour when clocks roll back,
-            # meaning clock hits midnight twice, then use the 2nd (ambiguous=True)
-            quotes.index = pd.to_datetime(quotes.index.date).tz_localize(tz_exchange, ambiguous=True, nonexistent='shift_forward')
-            if dividends.shape[0] > 0:
-                dividends.index = pd.to_datetime(dividends.index.date).tz_localize(tz_exchange, ambiguous=True, nonexistent='shift_forward')
-            if splits.shape[0] > 0:
-                splits.index = pd.to_datetime(splits.index.date).tz_localize(tz_exchange, ambiguous=True, nonexistent='shift_forward')
-
-        # Combine
-        df = quotes.sort_index()
-        if dividends.shape[0] > 0:
-            df = utils.safe_merge_dfs(df, dividends, interval)
-        if "Dividends" in df.columns:
-            df.loc[df["Dividends"].isna(), "Dividends"] = 0
-        else:
-            df["Dividends"] = 0.0
-        if splits.shape[0] > 0:
-            df = utils.safe_merge_dfs(df, splits, interval)
-        if "Stock Splits" in df.columns:
-            df.loc[df["Stock Splits"].isna(), "Stock Splits"] = 0
-        else:
-            df["Stock Splits"] = 0.0
-        if expect_capital_gains:
-            if capital_gains.shape[0] > 0:
-                df = utils.safe_merge_dfs(df, capital_gains, interval)
-            if "Capital Gains" in df.columns:
-                df.loc[df["Capital Gains"].isna(), "Capital Gains"] = 0
-            else:
-                df["Capital Gains"] = 0.0
-        logger.debug(f'{self.ticker}: OHLC after combining events: {quotes.index[0]} -> {quotes.index[-1]}')
-
-        df = df[~df.index.duplicated(keep='first')]  # must do before repair
-
-        if repair:
-            # Do this before auto/back adjust
-            logger.debug(f'{self.ticker}: checking OHLC for repairs ...')
-            df = self._fix_unit_mixups(df, interval, tz_exchange, prepost)
-            df = self._fix_bad_stock_split(df, interval, tz_exchange)
-            # Must repair 100x and split errors before price reconstruction
-            df = self._fix_zeroes(df, interval, tz_exchange, prepost)
-            df = self._fix_missing_div_adjust(df, interval, tz_exchange)
-            df = df.sort_index()
-
-        # Auto/back adjust
-        try:
-            if auto_adjust:
-                df = utils.auto_adjust(df)
-            elif back_adjust:
-                df = utils.back_adjust(df)
-        except Exception as e:
-            if auto_adjust:
-                err_msg = "auto_adjust failed with %s" % e
-            else:
-                err_msg = "back_adjust failed with %s" % e
-            shared._DFS[self.ticker] = utils.empty_df()
-            shared._ERRORS[self.ticker] = err_msg
-            if raise_errors:
-                raise Exception('%s: %s' % (self.ticker, err_msg))
-            else:
-                logger.error('%s: %s' % (self.ticker, err_msg))
-
-        if rounding:
-            df = np.round(df, data["chart"]["result"][0]["meta"]["priceHint"])
-        df['Volume'] = df['Volume'].fillna(0).astype(np.int64)
-
-        if intraday:
-            df.index.name = "Datetime"
-        else:
-            df.index.name = "Date"
-
-        self._history = df.copy()
-
-        # missing rows cleanup
-        if not actions:
-            df = df.drop(columns=["Dividends", "Stock Splits", "Capital Gains"], errors='ignore')
-        if not keepna:
-            data_colnames = price_colnames + ['Volume'] + ['Dividends', 'Stock Splits', 'Capital Gains']
-            data_colnames = [c for c in data_colnames if c in df.columns]
-            mask_nan_or_zero = (df[data_colnames].isna() | (df[data_colnames] == 0)).all(axis=1)
-            df = df.drop(mask_nan_or_zero.index[mask_nan_or_zero])
-
-        logger.debug(f'{self.ticker}: yfinance returning OHLC: {df.index[0]} -> {df.index[-1]}')
-
-        if self._reconstruct_start_interval is not None and self._reconstruct_start_interval == interval:
-            self._reconstruct_start_interval = None
-        return df
+    def history(self, *args, **kwargs) -> pd.DataFrame:
+        return self._lazy_load_price_history().history(*args, **kwargs)
 
     # ------------------------
 
-    @utils.log_indent_decorator
-    def _reconstruct_intervals_batch(self, df, interval, prepost, tag=-1):
-        # Reconstruct values in df using finer-grained price data. Delimiter marks what to reconstruct
-        logger = utils.get_yf_logger()
-
-        if not isinstance(df, pd.DataFrame):
-            raise Exception("'df' must be a Pandas DataFrame not", type(df))
-        if interval == "1m":
-            # Can't go smaller than 1m so can't reconstruct
-            return df
-
-        if interval[1:] in ['d', 'wk', 'mo']:
-            # Interday data always includes pre & post
-            prepost = True
-            intraday = False
-        else:
-            intraday = True
-
-        price_cols = [c for c in price_colnames if c in df]
-        data_cols = price_cols + ["Volume"]
-
-        # If interval is weekly then can construct with daily. But if smaller intervals then
-        # restricted to recent times:
-        intervals = ["1wk", "1d", "1h", "30m", "15m", "5m", "2m", "1m"]
-        itds = {i: utils._interval_to_timedelta(interval) for i in intervals}
-        nexts = {intervals[i]: intervals[i + 1] for i in range(len(intervals) - 1)}
-        min_lookbacks = {"1wk": None, "1d": None, "1h": _datetime.timedelta(days=730)}
-        for i in ["30m", "15m", "5m", "2m"]:
-            min_lookbacks[i] = _datetime.timedelta(days=60)
-        min_lookbacks["1m"] = _datetime.timedelta(days=30)
-        if interval in nexts:
-            sub_interval = nexts[interval]
-            td_range = itds[interval]
-        else:
-            logger.warning(f"Have not implemented price repair for '{interval}' interval. Contact developers")
-            if "Repaired?" not in df.columns:
-                df["Repaired?"] = False
-            return df
-
-        # Limit max reconstruction depth to 2:
-        if self._reconstruct_start_interval is None:
-            self._reconstruct_start_interval = interval
-        if interval != self._reconstruct_start_interval and interval != nexts[self._reconstruct_start_interval]:
-            logger.debug(f"{self.ticker}: Price repair has hit max depth of 2 ('%s'->'%s'->'%s')", self._reconstruct_start_interval, nexts[self._reconstruct_start_interval], interval)
-            return df
-
-        df = df.sort_index()
-
-        f_repair = df[data_cols].to_numpy() == tag
-        f_repair_rows = f_repair.any(axis=1)
-
-        # Ignore old intervals for which Yahoo won't return finer data:
-        m = min_lookbacks[sub_interval]
-        if m is None:
-            min_dt = None
-        else:
-            m -= _datetime.timedelta(days=1)  # allow space for 1-day padding
-            min_dt = pd.Timestamp.utcnow() - m
-            min_dt = min_dt.tz_convert(df.index.tz).ceil("D")
-        logger.debug(f"min_dt={min_dt} interval={interval} sub_interval={sub_interval}")
-        if min_dt is not None:
-            f_recent = df.index >= min_dt
-            f_repair_rows = f_repair_rows & f_recent
-            if not f_repair_rows.any():
-                logger.info("Data too old to repair")
-                if "Repaired?" not in df.columns:
-                    df["Repaired?"] = False
-                return df
-
-        dts_to_repair = df.index[f_repair_rows]
-
-        if len(dts_to_repair) == 0:
-            logger.info("Nothing needs repairing (dts_to_repair[] empty)")
-            if "Repaired?" not in df.columns:
-                df["Repaired?"] = False
-            return df
-
-        df_v2 = df.copy()
-        if "Repaired?" not in df_v2.columns:
-            df_v2["Repaired?"] = False
-        f_good = ~(df[price_cols].isna().any(axis=1))
-        f_good = f_good & (df[price_cols].to_numpy() != tag).all(axis=1)
-        df_good = df[f_good]
-
-        # Group nearby NaN-intervals together to reduce number of Yahoo fetches
-        dts_groups = [[dts_to_repair[0]]]
-        # Note on setting max size: have to allow space for adding good data
-        if sub_interval == "1mo":
-            grp_max_size = _dateutil.relativedelta.relativedelta(years=2)
-        elif sub_interval == "1wk":
-            grp_max_size = _dateutil.relativedelta.relativedelta(years=2)
-        elif sub_interval == "1d":
-            grp_max_size = _dateutil.relativedelta.relativedelta(years=2)
-        elif sub_interval == "1h":
-            grp_max_size = _dateutil.relativedelta.relativedelta(years=1)
-        elif sub_interval == "1m":
-            grp_max_size = _datetime.timedelta(days=5)  # allow 2 days for buffer below
-        else:
-            grp_max_size = _datetime.timedelta(days=30)
-        logger.debug(f"grp_max_size = {grp_max_size}")
-        for i in range(1, len(dts_to_repair)):
-            dt = dts_to_repair[i]
-            if dt.date() < dts_groups[-1][0].date() + grp_max_size:
-                dts_groups[-1].append(dt)
-            else:
-                dts_groups.append([dt])
-
-        logger.debug("Repair groups:")
-        for g in dts_groups:
-            logger.debug(f"- {g[0]} -> {g[-1]}")
-
-        # Add some good data to each group, so can calibrate prices later:
-        for i in range(len(dts_groups)):
-            g = dts_groups[i]
-            g0 = g[0]
-            i0 = df_good.index.get_indexer([g0], method="nearest")[0]
-            if i0 > 0:
-                if (min_dt is None or df_good.index[i0 - 1] >= min_dt) and \
-                        ((not intraday) or df_good.index[i0 - 1].date() == g0.date()):
-                    i0 -= 1
-            gl = g[-1]
-            il = df_good.index.get_indexer([gl], method="nearest")[0]
-            if il < len(df_good) - 1:
-                if (not intraday) or df_good.index[il + 1].date() == gl.date():
-                    il += 1
-            good_dts = df_good.index[i0:il + 1]
-            dts_groups[i] += good_dts.to_list()
-            dts_groups[i].sort()
-
-        n_fixed = 0
-        for g in dts_groups:
-            df_block = df[df.index.isin(g)]
-            logger.debug("df_block:\n" + str(df_block))
-
-            start_dt = g[0]
-            start_d = start_dt.date()
-            reject = False
-            if sub_interval == "1h" and (_datetime.date.today() - start_d) > _datetime.timedelta(days=729):
-                reject = True
-            elif sub_interval in ["30m", "15m"] and (_datetime.date.today() - start_d) > _datetime.timedelta(days=59):
-                reject = True
-            if reject:
-                # Don't bother requesting more price data, Yahoo will reject
-                msg = f"Cannot reconstruct {interval} block starting"
-                if intraday:
-                    msg += f" {start_dt}"
-                else:
-                    msg += f" {start_d}"
-                msg += ", too old, Yahoo will reject request for finer-grain data"
-                logger.info(msg)
-                continue
-
-            td_1d = _datetime.timedelta(days=1)
-            end_dt = g[-1]
-            end_d = end_dt.date() + td_1d
-            if interval in "1wk":
-                fetch_start = start_d - td_range  # need previous week too
-                fetch_end = g[-1].date() + td_range
-            elif interval == "1d":
-                fetch_start = start_d
-                fetch_end = g[-1].date() + td_range
-            else:
-                fetch_start = g[0]
-                fetch_end = g[-1] + td_range
-
-            # The first and last day returned by Yahoo can be slightly wrong, so add buffer:
-            fetch_start -= td_1d
-            fetch_end += td_1d
-            if intraday:
-                fetch_start = fetch_start.date()
-                fetch_end = fetch_end.date() + td_1d
-            if min_dt is not None:
-                fetch_start = max(min_dt.date(), fetch_start)
-            logger.debug(f"Fetching {sub_interval} prepost={prepost} {fetch_start}->{fetch_end}")
-            df_fine = self.history(start=fetch_start, end=fetch_end, interval=sub_interval, auto_adjust=False, actions=True, prepost=prepost, repair=True, keepna=True)
-            if df_fine is None or df_fine.empty:
-                msg = f"Cannot reconstruct {interval} block starting"
-                if intraday:
-                    msg += f" {start_dt}"
-                else:
-                    msg += f" {start_d}"
-                msg += ", too old, Yahoo is rejecting request for finer-grain data"
-                logger.debug(msg)
-                continue
-            # Discard the buffer
-            df_fine = df_fine.loc[g[0]: g[-1] + itds[sub_interval] - _datetime.timedelta(milliseconds=1)].copy()
-            if df_fine.empty:
-                msg = f"Cannot reconstruct {interval} block range"
-                if intraday:
-                    msg += f" {start_dt}->{end_dt}"
-                else:
-                    msg += f" {start_d}->{end_d}"
-                msg += ", Yahoo not returning finer-grain data within range"
-                logger.debug(msg)
-                continue
-
-            df_fine["ctr"] = 0
-            if interval == "1wk":
-                weekdays = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]
-                week_end_day = weekdays[(df_block.index[0].weekday() + 7 - 1) % 7]
-                df_fine["Week Start"] = df_fine.index.tz_localize(None).to_period("W-" + week_end_day).start_time
-                grp_col = "Week Start"
-            elif interval == "1d":
-                df_fine["Day Start"] = pd.to_datetime(df_fine.index.date)
-                grp_col = "Day Start"
-            else:
-                df_fine.loc[df_fine.index.isin(df_block.index), "ctr"] = 1
-                df_fine["intervalID"] = df_fine["ctr"].cumsum()
-                df_fine = df_fine.drop("ctr", axis=1)
-                grp_col = "intervalID"
-            df_fine = df_fine[~df_fine[price_cols + ['Dividends']].isna().all(axis=1)]
-
-            df_fine_grp = df_fine.groupby(grp_col)
-            df_new = df_fine_grp.agg(
-                Open=("Open", "first"),
-                Close=("Close", "last"),
-                AdjClose=("Adj Close", "last"),
-                Low=("Low", "min"),
-                High=("High", "max"),
-                Dividends=("Dividends", "sum"),
-                Volume=("Volume", "sum")).rename(columns={"AdjClose": "Adj Close"})
-            if grp_col in ["Week Start", "Day Start"]:
-                df_new.index = df_new.index.tz_localize(df_fine.index.tz)
-            else:
-                df_fine["diff"] = df_fine["intervalID"].diff()
-                new_index = np.append([df_fine.index[0]], df_fine.index[df_fine["intervalID"].diff() > 0])
-                df_new.index = new_index
-            logger.debug('df_new:' + '\n' + str(df_new))
-
-            # Calibrate!
-            common_index = np.intersect1d(df_block.index, df_new.index)
-            if len(common_index) == 0:
-                # Can't calibrate so don't attempt repair
-                logger.info(f"Can't calibrate {interval} block starting {start_d} so aborting repair")
-                continue
-            # First, attempt to calibrate the 'Adj Close' column. OK if cannot.
-            # Only necessary for 1d interval, because the 1h data is not div-adjusted.
-            if interval == '1d':
-                df_new_calib = df_new[df_new.index.isin(common_index)]
-                df_block_calib = df_block[df_block.index.isin(common_index)]
-                f_tag = df_block_calib['Adj Close'] == tag
-                if f_tag.any():
-                    div_adjusts = df_block_calib['Adj Close'] / df_block_calib['Close']
-                    # The loop below assumes each 1d repair is isolated, i.e. surrounded by
-                    # good data. Which is case most of time.
-                    # But in case are repairing a chunk of bad 1d data, back/forward-fill the
-                    # good div-adjustments - not perfect, but a good backup.
-                    div_adjusts[f_tag] = np.nan
-                    div_adjusts = div_adjusts.ffill().bfill()
-                    for idx in np.where(f_tag)[0]:
-                        dt = df_new_calib.index[idx]
-                        n = len(div_adjusts)
-                        if df_new.loc[dt, "Dividends"] != 0:
-                            if idx < n - 1:
-                                # Easy, take div-adjustment from next-day
-                                div_adjusts.iloc[idx] = div_adjusts.iloc[idx + 1]
-                            else:
-                                # Take previous-day div-adjustment and reverse todays adjustment
-                                div_adj = 1.0 - df_new_calib["Dividends"].iloc[idx] / df_new_calib['Close'].iloc[
-                                    idx - 1]
-                                div_adjusts.iloc[idx] = div_adjusts.iloc[idx - 1] / div_adj
-                        else:
-                            if idx > 0:
-                                # Easy, take div-adjustment from previous-day
-                                div_adjusts.iloc[idx] = div_adjusts.iloc[idx - 1]
-                            else:
-                                # Must take next-day div-adjustment
-                                div_adjusts.iloc[idx] = div_adjusts.iloc[idx + 1]
-                                if df_new_calib["Dividends"].iloc[idx + 1] != 0:
-                                    div_adjusts.iloc[idx] *= 1.0 - df_new_calib["Dividends"].iloc[idx + 1] / \
-                                                        df_new_calib['Close'].iloc[idx]
-                    f_close_bad = df_block_calib['Close'] == tag
-                    div_adjusts = div_adjusts.reindex(df_block.index, fill_value=np.nan).ffill().bfill()
-                    df_new['Adj Close'] = df_block['Close'] * div_adjusts
-                    if f_close_bad.any():
-                        f_close_bad_new = f_close_bad.reindex(df_new.index, fill_value=False)
-                        div_adjusts_new = div_adjusts.reindex(df_new.index, fill_value=np.nan).ffill().bfill()
-                        div_adjusts_new_np = f_close_bad_new.to_numpy()
-                        df_new.loc[div_adjusts_new_np, 'Adj Close'] = df_new['Close'][div_adjusts_new_np] * div_adjusts_new[div_adjusts_new_np]
-
-            # Check whether 'df_fine' has different split-adjustment.
-            # If different, then adjust to match 'df'
-            calib_cols = ['Open', 'Close']
-            df_new_calib = df_new[df_new.index.isin(common_index)][calib_cols].to_numpy()
-            df_block_calib = df_block[df_block.index.isin(common_index)][calib_cols].to_numpy()
-            calib_filter = (df_block_calib != tag)
-            if not calib_filter.any():
-                # Can't calibrate so don't attempt repair
-                logger.info(f"Can't calibrate {interval} block starting {start_d} so aborting repair")
-                continue
-            # Avoid divide-by-zero warnings:
-            for j in range(len(calib_cols)):
-                f = ~calib_filter[:, j]
-                if f.any():
-                    df_block_calib[f, j] = 1
-                    df_new_calib[f, j] = 1
-            ratios = df_block_calib[calib_filter] / df_new_calib[calib_filter]
-            weights = df_fine_grp.size()
-            weights.index = df_new.index
-            weights = weights[weights.index.isin(common_index)].to_numpy().astype(float)
-            weights = weights[:, None]  # transpose
-            weights = np.tile(weights, len(calib_cols))  # 1D -> 2D
-            weights = weights[calib_filter]  # flatten
-            not1 = ~np.isclose(ratios, 1.0, rtol=0.00001)
-            if np.sum(not1) == len(calib_cols):
-                # Only 1 calibration row in df_new is different to df_block so ignore
-                ratio = 1.0
-            else:
-                ratio = np.average(ratios, weights=weights)
-            logger.debug(f"Price calibration ratio (raw) = {ratio:6f}")
-            ratio_rcp = round(1.0 / ratio, 1)
-            ratio = round(ratio, 1)
-            if ratio == 1 and ratio_rcp == 1:
-                # Good!
-                pass
-            else:
-                if ratio > 1:
-                    # data has different split-adjustment than fine-grained data
-                    # Adjust fine-grained to match
-                    df_new[price_cols] *= ratio
-                    df_new["Volume"] /= ratio
-                elif ratio_rcp > 1:
-                    # data has different split-adjustment than fine-grained data
-                    # Adjust fine-grained to match
-                    df_new[price_cols] *= 1.0 / ratio_rcp
-                    df_new["Volume"] *= ratio_rcp
-
-            # Repair!
-            bad_dts = df_block.index[(df_block[price_cols + ["Volume"]] == tag).to_numpy().any(axis=1)]
-
-            no_fine_data_dts = []
-            for idx in bad_dts:
-                if idx not in df_new.index:
-                    # Yahoo didn't return finer-grain data for this interval,
-                    # so probably no trading happened.
-                    no_fine_data_dts.append(idx)
-            if len(no_fine_data_dts) > 0:
-                logger.debug("Yahoo didn't return finer-grain data for these intervals: " + str(no_fine_data_dts))
-            for idx in bad_dts:
-                if idx not in df_new.index:
-                    # Yahoo didn't return finer-grain data for this interval,
-                    # so probably no trading happened.
-                    continue
-                df_new_row = df_new.loc[idx]
-
-                if interval == "1wk":
-                    df_last_week = df_new.iloc[df_new.index.get_loc(idx) - 1]
-                    df_fine = df_fine.loc[idx:]
-
-                df_bad_row = df.loc[idx]
-                bad_fields = df_bad_row.index[df_bad_row == tag].to_numpy()
-                if "High" in bad_fields:
-                    df_v2.loc[idx, "High"] = df_new_row["High"]
-                if "Low" in bad_fields:
-                    df_v2.loc[idx, "Low"] = df_new_row["Low"]
-                if "Open" in bad_fields:
-                    if interval == "1wk" and idx != df_fine.index[0]:
-                        # Exchange closed Monday. In this case, Yahoo sets Open to last week close
-                        df_v2.loc[idx, "Open"] = df_last_week["Close"]
-                        df_v2.loc[idx, "Low"] = min(df_v2.loc[idx, "Open"], df_v2.loc[idx, "Low"])
-                    else:
-                        df_v2.loc[idx, "Open"] = df_new_row["Open"]
-                if "Close" in bad_fields:
-                    df_v2.loc[idx, "Close"] = df_new_row["Close"]
-                    # Assume 'Adj Close' also corrupted, easier than detecting whether true
-                    df_v2.loc[idx, "Adj Close"] = df_new_row["Adj Close"]
-                elif "Adj Close" in bad_fields:
-                    df_v2.loc[idx, "Adj Close"] = df_new_row["Adj Close"]
-                if "Volume" in bad_fields:
-                    df_v2.loc[idx, "Volume"] = df_new_row["Volume"]
-                df_v2.loc[idx, "Repaired?"] = True
-                n_fixed += 1
-
-        return df_v2
-
-    @utils.log_indent_decorator
-    def _fix_unit_mixups(self, df, interval, tz_exchange, prepost):
-        if df.empty:
-            return df
-        df2 = self._fix_unit_switch(df, interval, tz_exchange)
-        df3 = self._fix_unit_random_mixups(df2, interval, tz_exchange, prepost)
-        return df3
-
-    @utils.log_indent_decorator
-    def _fix_unit_random_mixups(self, df, interval, tz_exchange, prepost):
-        # Sometimes Yahoo returns few prices in cents/pence instead of $/£
-        # I.e. 100x bigger
-        # 2 ways this manifests:
-        # - random 100x errors spread throughout table
-        # - a sudden switch between $<->cents at some date
-        # This function fixes the first.
-
-        if df.empty:
-            return df
-
-        # Easy to detect and fix, just look for outliers = ~100x local median
-        logger = utils.get_yf_logger()
-
-        if df.shape[0] == 0:
-            if "Repaired?" not in df.columns:
-                df["Repaired?"] = False
-            return df
-        if df.shape[0] == 1:
-            # Need multiple rows to confidently identify outliers
-            logger.info("price-repair-100x: Cannot check single-row table for 100x price errors")
-            if "Repaired?" not in df.columns:
-                df["Repaired?"] = False
-            return df
-
-        df2 = df.copy()
-
-        if df2.index.tz is None:
-            df2.index = df2.index.tz_localize(tz_exchange)
-        elif df2.index.tz != tz_exchange:
-            df2.index = df2.index.tz_convert(tz_exchange)
-
-        # Only import scipy if users actually want function. To avoid
-        # adding it to dependencies.
-        from scipy import ndimage as _ndimage
-
-        data_cols = ["High", "Open", "Low", "Close", "Adj Close"]  # Order important, separate High from Low
-        data_cols = [c for c in data_cols if c in df2.columns]
-        f_zeroes = (df2[data_cols] == 0).any(axis=1).to_numpy()
-        if f_zeroes.any():
-            df2_zeroes = df2[f_zeroes]
-            df2 = df2[~f_zeroes]
-        else:
-            df2_zeroes = None
-        if df2.shape[0] <= 1:
-            logger.info("price-repair-100x: Insufficient good data for detecting 100x price errors")
-            if "Repaired?" not in df.columns:
-                df["Repaired?"] = False
-            return df
-        df2_data = df2[data_cols].to_numpy()
-        median = _ndimage.median_filter(df2_data, size=(3, 3), mode="wrap")
-        ratio = df2_data / median
-        ratio_rounded = (ratio / 20).round() * 20  # round ratio to nearest 20
-        f = ratio_rounded == 100
-        ratio_rcp = 1.0/ratio
-        ratio_rcp_rounded = (ratio_rcp / 20).round() * 20  # round ratio to nearest 20
-        f_rcp = (ratio_rounded == 100) | (ratio_rcp_rounded == 100)
-        f_either = f | f_rcp
-        if not f_either.any():
-            logger.info("price-repair-100x: No sporadic 100x errors")
-            if "Repaired?" not in df.columns:
-                df["Repaired?"] = False
-            return df
-
-        # Mark values to send for repair
-        tag = -1.0
-        for i in range(len(data_cols)):
-            fi = f_either[:, i]
-            c = data_cols[i]
-            df2.loc[fi, c] = tag
-
-        n_before = (df2_data == tag).sum()
-        df2 = self._reconstruct_intervals_batch(df2, interval, prepost, tag)
-        df2_tagged = df2[data_cols].to_numpy() == tag
-        n_after = (df2[data_cols].to_numpy() == tag).sum()
-
-        if n_after > 0:
-            # This second pass will *crudely* "fix" any remaining errors in High/Low
-            # simply by ensuring they don't contradict e.g. Low = 100x High.
-            f = (df2[data_cols].to_numpy() == tag) & f
-            for i in range(f.shape[0]):
-                fi = f[i, :]
-                if not fi.any():
-                    continue
-                idx = df2.index[i]
-
-                for c in ['Open', 'Close']:
-                    j = data_cols.index(c)
-                    if fi[j]:
-                        df2.loc[idx, c] = df.loc[idx, c] * 0.01
-
-                c = "High"
-                j = data_cols.index(c)
-                if fi[j]:
-                    df2.loc[idx, c] = df2.loc[idx, ["Open", "Close"]].max()
-
-                c = "Low"
-                j = data_cols.index(c)
-                if fi[j]:
-                    df2.loc[idx, c] = df2.loc[idx, ["Open", "Close"]].min()
-
-            f_rcp = (df2[data_cols].to_numpy() == tag) & f_rcp
-            for i in range(f_rcp.shape[0]):
-                fi = f_rcp[i, :]
-                if not fi.any():
-                    continue
-                idx = df2.index[i]
-
-                for c in ['Open', 'Close']:
-                    j = data_cols.index(c)
-                    if fi[j]:
-                        df2.loc[idx, c] = df.loc[idx, c] * 100.0
-
-                c = "High"
-                j = data_cols.index(c)
-                if fi[j]:
-                    df2.loc[idx, c] = df2.loc[idx, ["Open", "Close"]].max()
-
-                c = "Low"
-                j = data_cols.index(c)
-                if fi[j]:
-                    df2.loc[idx, c] = df2.loc[idx, ["Open", "Close"]].min()
-
-            df2_tagged = df2[data_cols].to_numpy() == tag
-            n_after_crude = df2_tagged.sum()
-        else:
-            n_after_crude = n_after
-
-        n_fixed = n_before - n_after_crude
-        n_fixed_crudely = n_after - n_after_crude
-        if n_fixed > 0:
-            report_msg = f"{self.ticker}: fixed {n_fixed}/{n_before} currency unit mixups "
-            if n_fixed_crudely > 0:
-                report_msg += f"({n_fixed_crudely} crudely) "
-            report_msg += f"in {interval} price data"
-            logger.info('price-repair-100x: ' + report_msg)
-
-        # Restore original values where repair failed
-        f_either = df2[data_cols].to_numpy() == tag
-        for j in range(len(data_cols)):
-            fj = f_either[:, j]
-            if fj.any():
-                c = data_cols[j]
-                df2.loc[fj, c] = df.loc[fj, c]
-        if df2_zeroes is not None:
-            if "Repaired?" not in df2_zeroes.columns:
-                df2_zeroes["Repaired?"] = False
-            df2 = pd.concat([df2, df2_zeroes]).sort_index()
-            df2.index = pd.to_datetime()
-
-        return df2
-
-    @utils.log_indent_decorator
-    def _fix_unit_switch(self, df, interval, tz_exchange):
-        # Sometimes Yahoo returns few prices in cents/pence instead of $/£
-        # I.e. 100x bigger
-        # 2 ways this manifests:
-        # - random 100x errors spread throughout table
-        # - a sudden switch between $<->cents at some date
-        # This function fixes the second.
-        # Eventually Yahoo fixes but could take them 2 weeks.
-
-        return self._fix_prices_sudden_change(df, interval, tz_exchange, 100.0)
-
-    @utils.log_indent_decorator
-    def _fix_zeroes(self, df, interval, tz_exchange, prepost):
-        # Sometimes Yahoo returns prices=0 or NaN when trades occurred.
-        # But most times when prices=0 or NaN returned is because no trades.
-        # Impossible to distinguish, so only attempt repair if few or rare.
-
-        if df.empty:
-            return df
-
-        logger = utils.get_yf_logger()
-
-        if df.shape[0] == 0:
-            if "Repaired?" not in df.columns:
-                df["Repaired?"] = False
-            return df
-
-        intraday = interval[-1] in ("m", 'h')
-
-        df = df.sort_index()  # important!
-        df2 = df.copy()
-
-        if df2.index.tz is None:
-            df2.index = df2.index.tz_localize(tz_exchange)
-        elif df2.index.tz != tz_exchange:
-            df2.index = df2.index.tz_convert(tz_exchange)
-
-        price_cols = [c for c in price_colnames if c in df2.columns]
-        f_prices_bad = (df2[price_cols] == 0.0) | df2[price_cols].isna()
-        df2_reserve = None
-        if intraday:
-            # Ignore days with >50% intervals containing NaNs
-            grp = pd.Series(f_prices_bad.any(axis=1), name="nan").groupby(f_prices_bad.index.date)
-            nan_pct = grp.sum() / grp.count()
-            dts = nan_pct.index[nan_pct > 0.5]
-            f_zero_or_nan_ignore = np.isin(f_prices_bad.index.date, dts)
-            df2_reserve = df2[f_zero_or_nan_ignore]
-            df2 = df2[~f_zero_or_nan_ignore]
-            f_prices_bad = (df2[price_cols] == 0.0) | df2[price_cols].isna()
-
-        f_high_low_good = (~df2["High"].isna().to_numpy()) & (~df2["Low"].isna().to_numpy())
-        f_change = df2["High"].to_numpy() != df2["Low"].to_numpy()
-        f_vol_bad = (df2["Volume"] == 0).to_numpy() & f_high_low_good & f_change
-
-        # If stock split occurred, then trading must have happened.
-        # I should probably rename the function, because prices aren't zero ...
-        if 'Stock Splits' in df2.columns:
-            f_split = (df2['Stock Splits'] != 0.0).to_numpy()
-            if f_split.any():
-                f_change_expected_but_missing = f_split & ~f_change
-                if f_change_expected_but_missing.any():
-                    f_prices_bad[f_change_expected_but_missing] = True
-
-        # Check whether worth attempting repair
-        f_prices_bad = f_prices_bad.to_numpy()
-        f_bad_rows = f_prices_bad.any(axis=1) | f_vol_bad
-        if not f_bad_rows.any():
-            logger.info("price-repair-missing: No price=0 errors to repair")
-            if "Repaired?" not in df.columns:
-                df["Repaired?"] = False
-            return df
-        if f_prices_bad.sum() == len(price_cols) * len(df2):
-            # Need some good data to calibrate
-            logger.info("price-repair-missing: No good data for calibration so cannot fix price=0 bad data")
-            if "Repaired?" not in df.columns:
-                df["Repaired?"] = False
-            return df
-
-        data_cols = price_cols + ["Volume"]
-
-        # Mark values to send for repair
-        tag = -1.0
-        for i in range(len(price_cols)):
-            c = price_cols[i]
-            df2.loc[f_prices_bad[:, i], c] = tag
-        df2.loc[f_vol_bad, "Volume"] = tag
-        # If volume=0 or NaN for bad prices, then tag volume for repair
-        f_vol_zero_or_nan = (df2["Volume"].to_numpy() == 0) | (df2["Volume"].isna().to_numpy())
-        df2.loc[f_prices_bad.any(axis=1) & f_vol_zero_or_nan, "Volume"] = tag
-        # If volume=0 or NaN but price moved in interval, then tag volume for repair
-        df2.loc[f_change & f_vol_zero_or_nan, "Volume"] = tag
-
-        df2_tagged = df2[data_cols].to_numpy() == tag
-        n_before = df2_tagged.sum()
-        dts_tagged = df2.index[df2_tagged.any(axis=1)]
-        df2 = self._reconstruct_intervals_batch(df2, interval, prepost, tag)
-        df2_tagged = df2[data_cols].to_numpy() == tag
-        n_after = df2_tagged.sum()
-        dts_not_repaired = df2.index[df2_tagged.any(axis=1)]
-        n_fixed = n_before - n_after
-        if n_fixed > 0:
-            msg = f"{self.ticker}: fixed {n_fixed}/{n_before} value=0 errors in {interval} price data"
-            if n_fixed < 4:
-                dts_repaired = sorted(list(set(dts_tagged).difference(dts_not_repaired)))
-                msg += f": {dts_repaired}"
-            logger.info('price-repair-missing: ' + msg)
-
-        if df2_reserve is not None:
-            if "Repaired?" not in df2_reserve.columns:
-                df2_reserve["Repaired?"] = False
-            df2 = pd.concat([df2, df2_reserve]).sort_index()
-
-        # Restore original values where repair failed (i.e. remove tag values)
-        f = df2[data_cols].to_numpy() == tag
-        for j in range(len(data_cols)):
-            fj = f[:, j]
-            if fj.any():
-                c = data_cols[j]
-                df2.loc[fj, c] = df.loc[fj, c]
-
-        return df2
-
-    @utils.log_indent_decorator
-    def _fix_missing_div_adjust(self, df, interval, tz_exchange):
-        # Sometimes, if a dividend occurred today, then Yahoo has not adjusted historic data.
-        # Easy to detect and correct BUT ONLY IF the data 'df' includes today's dividend.
-        # E.g. if fetching historic prices before todays dividend, then cannot fix.
-
-        if df.empty:
-            return df
-
-        logger = utils.get_yf_logger()
-
-        if df is None or df.empty:
-            return df
-        interday = interval in ['1d', '1wk', '1mo', '3mo']
-        if not interday:
-            return df
-
-        df = df.sort_index()
-
-        f_div = (df["Dividends"] != 0.0).to_numpy()
-        if not f_div.any():
-            logger.debug('div-adjust-repair: No dividends to check')
-            return df
-
-        df2 = df.copy()
-        if df2.index.tz is None:
-            df2.index = df2.index.tz_localize(tz_exchange)
-        elif df2.index.tz != tz_exchange:
-            df2.index = df2.index.tz_convert(tz_exchange)
-
-        div_indices = np.where(f_div)[0]
-        last_div_idx = div_indices[-1]
-        if last_div_idx == 0:
-            # Not enough data to recalculate the div-adjustment,
-            # because need close day before
-            logger.debug('div-adjust-repair: Insufficient data to recalculate div-adjustment')
-            return df
-
-        # To determine if Yahoo messed up, analyse price data between today's dividend and
-        # the previous dividend
-        if len(div_indices) == 1:
-            # No other divs in data
-            prev_idx = 0
-            prev_dt = None
-        else:
-            prev_idx = div_indices[-2]
-            prev_dt = df2.index[prev_idx]
-        f_no_adj = (df2['Close'] == df2['Adj Close']).to_numpy()[prev_idx:last_div_idx]
-        threshold_pct = 0.5
-        Yahoo_failed = (np.sum(f_no_adj) / len(f_no_adj)) > threshold_pct
-
-        # Fix Yahoo
-        if Yahoo_failed:
-            last_div_dt = df2.index[last_div_idx]
-            last_div_row = df2.loc[last_div_dt]
-            close_day_before = df2['Close'].iloc[last_div_idx - 1]
-            adj = 1.0 - df2['Dividends'].iloc[last_div_idx] / close_day_before
-            div = last_div_row['Dividends']
-            msg = f'Correcting missing div-adjustment preceding div = {div} @ {last_div_dt.date()} (prev_dt={prev_dt})'
-            logger.debug('div-adjust-repair: ' + msg)
-
-            if interval == '1d':
-                # exclusive
-                df2.loc[:last_div_dt - _datetime.timedelta(seconds=1), 'Adj Close'] *= adj
-            else:
-                # inclusive
-                df2.loc[:last_div_dt, 'Adj Close'] *= adj
-
-        return df2
-
-    @utils.log_indent_decorator
-    def _fix_bad_stock_split(self, df, interval, tz_exchange):
-        # Repair idea is to look for BIG daily price changes that closely match the
-        # most recent stock split ratio. This indicates Yahoo failed to apply a new
-        # stock split to old price data.
-        #
-        # There is a slight complication, because Yahoo does another stupid thing.
-        # Sometimes the old data is adjusted twice. So cannot simply assume
-        # which direction to reverse adjustment - have to analyse prices and detect.
-        # Not difficult.
-
-        if df.empty:
-            return df
-
-        logger = utils.get_yf_logger()
-
-        interday = interval in ['1d', '1wk', '1mo', '3mo']
-        if not interday:
-            return df
-
-        # Find the most recent stock split
-        df = df.sort_index(ascending=False)
-        split_f = df['Stock Splits'].to_numpy() != 0
-        if not split_f.any():
-            logger.debug('price-repair-split: No splits in data')
-            return df
-        most_recent_split_day = df.index[split_f].max()
-        split = df.loc[most_recent_split_day, 'Stock Splits']
-        if most_recent_split_day == df.index[0]:
-            logger.info(
-                "price-repair-split: Need 1+ day of price data after split to determine true price. Won't repair")
-            return df
-
-        logger.debug(f'price-repair-split: Most recent split = {split:.4f} @ {most_recent_split_day.date()}')
-
-        return self._fix_prices_sudden_change(df, interval, tz_exchange, split, correct_volume=True)
-
-    @utils.log_indent_decorator
-    def _fix_prices_sudden_change(self, df, interval, tz_exchange, change, correct_volume=False):
-        if df.empty:
-            return df
-
-        logger = utils.get_yf_logger()
-
-        df = df.sort_index(ascending=False)
-        split = change
-        split_rcp = 1.0 / split
-        interday = interval in ['1d', '1wk', '1mo', '3mo']
-
-        if change in [100.0, 0.01]:
-            fix_type = '100x error'
-            start_min = None
-        else:
-            fix_type = 'bad split'
-            # start_min = 1 year before oldest split
-            f = df['Stock Splits'].to_numpy() != 0.0
-            start_min = (df.index[f].min() - _dateutil.relativedelta.relativedelta(years=1)).date()
-            logger.debug(f'price-repair-split: start_min={start_min}')
-
-        OHLC = ['Open', 'High', 'Low', 'Close']
-
-        # Do not attempt repair of the split is small,
-        # could be mistaken for normal price variance
-        if 0.8 < split < 1.25:
-            logger.info("price-repair-split: Split ratio too close to 1. Won't repair")
-            return df
-
-        df2 = df.copy()
-        if df2.index.tz is None:
-            df2.index = df2.index.tz_localize(tz_exchange)
-        elif df2.index.tz != tz_exchange:
-            df2.index = df2.index.tz_convert(tz_exchange)
-        n = df2.shape[0]
-
-        # If stock is currently suspended and not in USA, then usually Yahoo introduces
-        # 100x errors into suspended intervals. Clue is no price change and 0 volume.
-        # Better to use last active trading interval as baseline.
-        f_no_activity = (df2['Low'] == df2['High']) & (df2['Volume']==0)
-        f_no_activity = f_no_activity | df2[OHLC].isna().all(axis=1)
-        appears_suspended = f_no_activity.any() and np.where(f_no_activity)[0][0]==0
-        f_active = ~f_no_activity
-        idx_latest_active = np.where(f_active & np.roll(f_active, 1))[0]
-        if len(idx_latest_active) == 0:
-            idx_latest_active = None
-        else:
-            idx_latest_active = int(idx_latest_active[0])
-        log_msg = f'price-repair-split: appears_suspended={appears_suspended}, idx_latest_active={idx_latest_active}'
-        if idx_latest_active is not None:
-            log_msg += f' ({df.index[idx_latest_active].date()})'
-        logger.debug(log_msg)
-
-        if logger.isEnabledFor(logging.DEBUG):
-            df_debug = df2.copy()
-            df_debug = df_debug.drop(['Adj Close', 'Volume', 'Dividends', 'Repaired?'], axis=1, errors='ignore')
-            debug_cols = ['Low', 'High']
-            df_debug = df_debug.drop([c for c in OHLC if c not in debug_cols], axis=1, errors='ignore')
-        else:
-            debug_cols = []
-
-        # Calculate daily price % change. To reduce effect of price volatility,
-        # calculate change for each OHLC column.
-        if interday and interval != '1d' and split not in [100.0, 100, 0.001]:
-            # Avoid using 'Low' and 'High'. For multiday intervals, these can be
-            # very volatile so reduce ability to detect genuine stock split errors
-            _1d_change_x = np.full((n, 2), 1.0)
-            price_data = df2[['Open','Close']].to_numpy()
-            f_zero = price_data == 0.0
-        else:
-            _1d_change_x = np.full((n, 4), 1.0)
-            price_data = df2[OHLC].to_numpy()
-            f_zero = price_data == 0.0
-        if f_zero.any():
-            price_data[f_zero] = 1.0
-
-        # Update: if a VERY large dividend is paid out, then can be mistaken for a 1:2 stock split.
-        # Fix = use adjusted prices
-        adj = df2['Adj Close'].to_numpy() / df2['Close'].to_numpy()
-        df_dtype = price_data.dtype
-        if df_dtype == np.int64:
-            price_data = price_data.astype('float')
-        for j in range(price_data.shape[1]):
-            price_data[:,j] *= adj
-        if df_dtype == np.int64:
-            price_data = price_data.astype('int')
-
-        _1d_change_x[1:] = price_data[1:, ] / price_data[:-1, ]
-        f_zero_num_denom = f_zero | np.roll(f_zero, 1, axis=0)
-        if f_zero_num_denom.any():
-            _1d_change_x[f_zero_num_denom] = 1.0
-        if interday and interval != '1d':
-            # average change
-            _1d_change_minx = np.average(_1d_change_x, axis=1)
-        else:
-            # change nearest to 1.0
-            diff = np.abs(_1d_change_x - 1.0)
-            j_indices = np.argmin(diff, axis=1)
-            _1d_change_minx = _1d_change_x[np.arange(n), j_indices]
-        f_na = np.isnan(_1d_change_minx)
-        if f_na.any():
-            # Possible if data was too old for reconstruction.
-            _1d_change_minx[f_na] = 1.0
-        if logger.isEnabledFor(logging.DEBUG):
-            df_debug['1D change X'] = _1d_change_minx
-            df_debug['1D change X'] = df_debug['1D change X'].round(2).astype('str')
-
-        # If all 1D changes are closer to 1.0 than split, exit
-        split_max = max(split, split_rcp)
-        if np.max(_1d_change_minx) < (split_max - 1) * 0.5 + 1 and np.min(_1d_change_minx) > 1.0 / ((split_max - 1) * 0.5 + 1):
-            logger.info(f"price-repair-split: No {fix_type}s detected")
-            return df
-
-        # Calculate the true price variance, i.e. remove effect of bad split-adjustments.
-        # Key = ignore 1D changes outside of interquartile range
-        q1, q3 = np.percentile(_1d_change_minx, [25, 75])
-        iqr = q3 - q1
-        lower_bound = q1 - 1.5 * iqr
-        upper_bound = q3 + 1.5 * iqr
-        f = (_1d_change_minx >= lower_bound) & (_1d_change_minx <= upper_bound)
-        avg = np.mean(_1d_change_minx[f])
-        sd = np.std(_1d_change_minx[f])
-        # Now can calculate SD as % of mean
-        sd_pct = sd / avg
-        logger.debug(f"price-repair-split: Estimation of true 1D change stats: mean = {avg:.2f}, StdDev = {sd:.4f} ({sd_pct*100.0:.1f}% of mean)")
-
-        # Only proceed if split adjustment far exceeds normal 1D changes
-        largest_change_pct = 5 * sd_pct
-        if interday and interval != '1d':
-            largest_change_pct *= 3
-            if interval in ['1mo', '3mo']:
-                largest_change_pct *= 2
-        if max(split, split_rcp) < 1.0 + largest_change_pct:
-            logger.info("price-repair-split: Split ratio too close to normal price volatility. Won't repair")
-            logger.debug(f"sd_pct = {sd_pct:.4f}  largest_change_pct = {largest_change_pct:.4f}")
-            if logger.isEnabledFor(logging.DEBUG):
-                logger.debug(f"sd_pct = {sd_pct:.4f}  largest_change_pct = {largest_change_pct:.4f}")
-            return df
-
-        # Now can detect bad split adjustments
-        # Set threshold to halfway between split ratio and largest expected normal price change
-        r = _1d_change_minx / split_rcp
-        split_max = max(split, split_rcp)
-        logger.debug(f"price-repair-split: split_max={split_max:.3f} largest_change_pct={largest_change_pct:.4f}")
-        threshold = (split_max + 1.0 + largest_change_pct) * 0.5
-        logger.debug(f"price-repair-split: threshold={threshold:.3f}")
-
-        if 'Repaired?' not in df2.columns:
-            df2['Repaired?'] = False
-
-        if interday and interval != '1d':
-            # Yahoo creates multi-day intervals using potentiall corrupt data, e.g.
-            # the Close could be 100x Open. This means have to correct each OHLC column
-            # individually
-            correct_columns_individually = True
-        else:
-            correct_columns_individually = False
-
-        if correct_columns_individually:
-            _1d_change_x = np.full((n, 4), 1.0)
-            price_data = df2[OHLC].replace(0.0, 1.0).to_numpy()
-            _1d_change_x[1:] = price_data[1:, ] / price_data[:-1, ]
-        else:
-            _1d_change_x = _1d_change_minx
-
-        r = _1d_change_x / split_rcp
-        f_down = _1d_change_x < 1.0 / threshold
-        f_up = _1d_change_x > threshold
-        f = f_down | f_up
-        if logger.isEnabledFor(logging.DEBUG):
-            if not correct_columns_individually:
-                df_debug['r'] = r
-                df_debug['f_down'] = f_down
-                df_debug['f_up'] = f_up
-                df_debug['r'] = df_debug['r'].round(2).astype('str')
-            else:
-                for j in range(len(OHLC)):
-                    c = OHLC[j]
-                    if c in debug_cols:
-                        df_debug[c + '_r'] = r[:, j]
-                        df_debug[c + '_f_down'] = f_down[:, j]
-                        df_debug[c + '_f_up'] = f_up[:, j]
-                        df_debug[c + '_r'] = df_debug[c + '_r'].round(2).astype('str')
-
-
-        if not f.any():
-            logger.info(f'price-repair-split: No {fix_type}s detected')
-            return df
-
-        # Update: if any 100x changes are soon after a stock split, so could be confused with split error, then abort
-        threshold_days = 30
-        f_splits = df['Stock Splits'].to_numpy() != 0.0
-        if change in [100.0, 0.01] and f_splits.any():
-            indices_A = np.where(f_splits)[0]
-            indices_B = np.where(f)[0]
-            if not len(indices_A) or not len(indices_B):
-                return None
-            gaps = indices_B[:, None] - indices_A
-            # Because data is sorted in DEscending order, need to flip gaps
-            gaps *= -1
-            f_pos = gaps > 0
-            if f_pos.any():
-                gap_min = gaps[f_pos].min()
-                gap_td = utils._interval_to_timedelta(interval) * gap_min
-                if isinstance(gap_td, _dateutil.relativedelta.relativedelta):
-                    threshold = _dateutil.relativedelta.relativedelta(days=threshold_days)
-                else:
-                    threshold = _datetime.timedelta(days=threshold_days)
-                if gap_td < threshold:
-                    logger.info('price-repair-split: 100x changes are too soon after stock split events, aborting')
-                    return df
-
-        # if logger.isEnabledFor(logging.DEBUG):
-        #     df_debug['i'] = list(range(0, df_debug.shape[0]))
-        #     df_debug['i_rev'] = df_debug.shape[0]-1 - df_debug['i']
-        #     with pd.option_context('display.max_rows', None, 'display.max_columns', 10, 'display.width', 1000):  # more options can be specified also
-        #         logger.debug(f"price-repair-split: my workings:" + '\n' + str(df_debug))
-
-        def map_signals_to_ranges(f, f_up, f_down):
-            # Ensure 0th element is False, because True is nonsense
-            if f[0]:
-                f = np.copy(f)
-                f[0] = False
-                f_up = np.copy(f_up)
-                f_up[0] = False
-                f_down = np.copy(f_down)
-                f_down[0] = False
-
-            if not f.any():
-                return []
-
-            true_indices = np.where(f)[0]
-            ranges = []
-
-            for i in range(len(true_indices) - 1):
-                if i % 2 == 0:
-                    if split > 1.0:
-                        adj = 'split' if f_down[true_indices[i]] else '1.0/split'
-                    else:
-                        adj = '1.0/split' if f_down[true_indices[i]] else 'split'
-                    ranges.append((true_indices[i], true_indices[i + 1], adj))
-
-            if len(true_indices) % 2 != 0:
-                if split > 1.0:
-                    adj = 'split' if f_down[true_indices[-1]] else '1.0/split'
-                else:
-                    adj = '1.0/split' if f_down[true_indices[-1]] else 'split'
-                ranges.append((true_indices[-1], len(f), adj))
-
-            return ranges
-
-        if idx_latest_active is not None:
-            idx_rev_latest_active = df.shape[0] - 1 - idx_latest_active
-            logger.debug(f'price-repair-split: idx_latest_active={idx_latest_active}, idx_rev_latest_active={idx_rev_latest_active}')
-        if correct_columns_individually:
-            f_corrected = np.full(n, False)
-            if correct_volume:
-                # If Open or Close is repaired but not both,
-                # then this means the interval has a mix of correct
-                # and errors. A problem for correcting Volume,
-                # so use a heuristic:
-                # - if both Open & Close were Nx bad => Volume is Nx bad
-                # - if only one of Open & Close are Nx bad => Volume is 0.5*Nx bad
-                f_open_fixed = np.full(n, False)
-                f_close_fixed = np.full(n, False)
-
-            OHLC_correct_ranges = [None, None, None, None]
-            for j in range(len(OHLC)):
-                c = OHLC[j]
-                idx_first_f = np.where(f)[0][0]
-                if appears_suspended and (idx_latest_active is not None and idx_latest_active >= idx_first_f):
-                    # Suspended midway during data date range.
-                    # 1: process data before suspension in index-ascending (date-descending) order.
-                    # 2: process data after suspension in index-descending order. Requires signals to be reversed,
-                    #    then returned ranges to also be reversed, because this logic was originally written for
-                    #    index-ascending (date-descending) order.
-                    fj = f[:, j]
-                    f_upj = f_up[:, j]
-                    f_downj = f_down[:, j]
-                    ranges_before = map_signals_to_ranges(fj[idx_latest_active:], f_upj[idx_latest_active:], f_downj[idx_latest_active:])
-                    if len(ranges_before) > 0:
-                        # Shift each range back to global indexing
-                        for i in range(len(ranges_before)):
-                            r = ranges_before[i]
-                            ranges_before[i] = (r[0] + idx_latest_active, r[1] + idx_latest_active, r[2])
-                    f_rev_downj = np.flip(np.roll(f_upj, -1))  # correct
-                    f_rev_upj = np.flip(np.roll(f_downj, -1))  # correct
-                    f_revj = f_rev_upj | f_rev_downj
-                    ranges_after = map_signals_to_ranges(f_revj[idx_rev_latest_active:], f_rev_upj[idx_rev_latest_active:], f_rev_downj[idx_rev_latest_active:])
-                    if len(ranges_after) > 0:
-                        # Shift each range back to global indexing:
-                        for i in range(len(ranges_after)):
-                            r = ranges_after[i]
-                            ranges_after[i] = (r[0] + idx_rev_latest_active, r[1] + idx_rev_latest_active, r[2])
-                        # Flip range to normal ordering
-                        for i in range(len(ranges_after)):
-                            r = ranges_after[i]
-                            ranges_after[i] = (n-r[1], n-r[0], r[2])
-                    ranges = ranges_before
-                    ranges.extend(ranges_after)
-                else:
-                    ranges = map_signals_to_ranges(f[:, j], f_up[:, j], f_down[:, j])
-                logger.debug(f"column '{c}' ranges: {ranges}")
-                if start_min is not None:
-                    # Prune ranges that are older than start_min
-                    for i in range(len(ranges)-1, -1, -1):
-                        r = ranges[i]
-                        if df.index[r[0]].date() < start_min:
-                            logger.debug(f'price-repair-split: Pruning {c} range {df.index[r[0]]}->{df.index[r[1]-1]} because too old.')
-                            del ranges[i]
-
-                if len(ranges) > 0:
-                    OHLC_correct_ranges[j] = ranges
-
-            count = sum([1 if x is not None else 0 for x in OHLC_correct_ranges])
-            if count == 0:
-                pass
-            elif count == 1:
-                # If only 1 column then assume false positive
-                idxs = [i if OHLC_correct_ranges[i] else -1 for i in range(len(OHLC))]
-                idx = np.where(np.array(idxs) != -1)[0][0]
-                col = OHLC[idx]
-                logger.debug(f'price-repair-split: Potential {fix_type} detected only in column {col}, so treating as false positive (ignore)')
-            else:
-                # Only correct if at least 2 columns require correction.
-                for j in range(len(OHLC)):
-                    c = OHLC[j]
-                    ranges = OHLC_correct_ranges[j]
-                    if ranges is None:
-                        ranges = []
-                    for r in ranges:
-                        if r[2] == 'split':
-                            m = split
-                            m_rcp = split_rcp
-                        else:
-                            m = split_rcp
-                            m_rcp = split
-                        if interday:
-                            logger.info(f"price-repair-split: Corrected {fix_type} on col={c} range=[{df2.index[r[1]-1].date()}:{df2.index[r[0]].date()}] m={m:.4f}")
-                        else:
-                            logger.info(f"price-repair-split: Corrected {fix_type} on col={c} range=[{df2.index[r[1]-1]}:{df2.index[r[0]]}] m={m:.4f}")
-                        df2.iloc[r[0]:r[1], df2.columns.get_loc(c)] *= m
-                        if c == 'Close':
-                            df2.iloc[r[0]:r[1], df2.columns.get_loc('Adj Close')] *= m
-                        if correct_volume:
-                            if c == 'Open':
-                                f_open_fixed[r[0]:r[1]] = True
-                            elif c == 'Close':
-                                f_close_fixed[r[0]:r[1]] = True
-                        f_corrected[r[0]:r[1]] = True
-
-            if correct_volume:
-                f_open_and_closed_fixed = f_open_fixed & f_close_fixed
-                f_open_xor_closed_fixed = np.logical_xor(f_open_fixed, f_close_fixed)
-                if f_open_and_closed_fixed.any():
-                    df2.loc[f_open_and_closed_fixed, "Volume"] *= m_rcp
-                if f_open_xor_closed_fixed.any():
-                    df2.loc[f_open_xor_closed_fixed, "Volume"] *= 0.5 * m_rcp
-
-            df2.loc[f_corrected, 'Repaired?'] = True
-
-        else:
-            idx_first_f = np.where(f)[0][0]
-            if appears_suspended and (idx_latest_active is not None and idx_latest_active >= idx_first_f):
-                # Suspended midway during data date range.
-                # 1: process data before suspension in index-ascending (date-descending) order.
-                # 2: process data after suspension in index-descending order. Requires signals to be reversed,
-                #    then returned ranges to also be reversed, because this logic was originally written for
-                #    index-ascending (date-descending) order.
-                ranges_before = map_signals_to_ranges(f[idx_latest_active:], f_up[idx_latest_active:], f_down[idx_latest_active:])
-                if len(ranges_before) > 0:
-                    # Shift each range back to global indexing
-                    for i in range(len(ranges_before)):
-                        r = ranges_before[i]
-                        ranges_before[i] = (r[0] + idx_latest_active, r[1] + idx_latest_active, r[2])
-                f_rev_down = np.flip(np.roll(f_up, -1))
-                f_rev_up = np.flip(np.roll(f_down, -1))
-                f_rev = f_rev_up | f_rev_down
-                ranges_after = map_signals_to_ranges(f_rev[idx_rev_latest_active:], f_rev_up[idx_rev_latest_active:], f_rev_down[idx_rev_latest_active:])
-                if len(ranges_after) > 0:
-                    # Shift each range back to global indexing:
-                    for i in range(len(ranges_after)):
-                        r = ranges_after[i]
-                        ranges_after[i] = (r[0] + idx_rev_latest_active, r[1] + idx_rev_latest_active, r[2])
-                    # Flip range to normal ordering
-                    for i in range(len(ranges_after)):
-                        r = ranges_after[i]
-                        ranges_after[i] = (n-r[1], n-r[0], r[2])
-                ranges = ranges_before
-                ranges.extend(ranges_after)
-            else:
-                ranges = map_signals_to_ranges(f, f_up, f_down)
-            if start_min is not None:
-                # Prune ranges that are older than start_min
-                for i in range(len(ranges)-1, -1, -1):
-                    r = ranges[i]
-                    if df.index[r[0]].date() < start_min:
-                        logger.debug(f'price-repair-split: Pruning range {df.index[r[0]]}->{df.index[r[1]-1]} because too old.')
-                        del ranges[i]
-            for r in ranges:
-                if r[2] == 'split':
-                    m = split
-                    m_rcp = split_rcp
-                else:
-                    m = split_rcp
-                    m_rcp = split
-                logger.debug(f"price-repair-split: range={r} m={m}")
-                for c in ['Open', 'High', 'Low', 'Close', 'Adj Close']:
-                    df2.iloc[r[0]:r[1], df2.columns.get_loc(c)] *= m
-                if correct_volume:
-                    df2.iloc[r[0]:r[1], df2.columns.get_loc("Volume")] *= m_rcp
-                df2.iloc[r[0]:r[1], df2.columns.get_loc('Repaired?')] = True
-                if r[0] == r[1] - 1:
-                    if interday:
-                        msg = f"price-repair-split: Corrected {fix_type} on interval {df2.index[r[0]].date()}"
-                    else:
-                        msg = f"price-repair-split: Corrected {fix_type} on interval {df2.index[r[0]]}"
-                else:
-                    # Note: df2 sorted with index descending
-                    start = df2.index[r[1] - 1]
-                    end = df2.index[r[0]]
-                    if interday:
-                        msg = f"price-repair-split: Corrected {fix_type} across intervals {start.date()} -> {end.date()} (inclusive)"
-                    else:
-                        msg = f"price-repair-split: Corrected {fix_type} across intervals {start} -> {end} (inclusive)"
-                logger.info(msg)
-
-        if correct_volume:
-            f_na = df2['Volume'].isna()
-            if f_na.any():
-                df2.loc[~f_na,'Volume'] = df2['Volume'][~f_na].round(0).astype('int')
-            else:
-                df2['Volume'] = df2['Volume'].round(0).astype('int')
-
-        return df2
+    def _lazy_load_price_history(self):
+        if self._price_history is None:
+            self._price_history = PriceHistory(self._data, self.ticker, self._get_ticker_tz(self.proxy, timeout=10))
+        return self._price_history
 
     def _get_ticker_tz(self, proxy, timeout):
         proxy = proxy or self.proxy
@@ -1678,7 +116,7 @@ def _fetch_ticker_tz(self, proxy, timeout):
         params = {"range": "1d", "interval": "1d"}
 
         # Getting data from json
-        url = f"{self._base_url}/v8/finance/chart/{self.ticker}"
+        url = f"{_BASE_URL_}/v8/finance/chart/{self.ticker}"
 
         try:
             data = self._data.cache_get(url=url, params=params, proxy=proxy, timeout=timeout)
@@ -1952,39 +390,16 @@ def get_cashflow(self, proxy=None, as_dict=False, pretty=False, freq="yearly"):
         return self.get_cash_flow(proxy, as_dict, pretty, freq)
 
     def get_dividends(self, proxy=None) -> pd.Series:
-        if self._history is None:
-            self.history(period="max", proxy=proxy)
-        if self._history is not None and "Dividends" in self._history:
-            dividends = self._history["Dividends"]
-            return dividends[dividends != 0]
-        return pd.Series()
+        return self._lazy_load_price_history().get_dividends(proxy)
 
     def get_capital_gains(self, proxy=None) -> pd.Series:
-        if self._history is None:
-            self.history(period="max", proxy=proxy)
-        if self._history is not None and "Capital Gains" in self._history:
-            capital_gains = self._history["Capital Gains"]
-            return capital_gains[capital_gains != 0]
-        return _empty_series
+        return self._lazy_load_price_history().get_capital_gains(proxy)
 
     def get_splits(self, proxy=None) -> pd.Series:
-        if self._history is None:
-            self.history(period="max", proxy=proxy)
-        if self._history is not None and "Stock Splits" in self._history:
-            splits = self._history["Stock Splits"]
-            return splits[splits != 0]
-        return pd.Series()
+        return self._lazy_load_price_history().get_splits(proxy)
 
     def get_actions(self, proxy=None) -> pd.Series:
-        if self._history is None:
-            self.history(period="max", proxy=proxy)
-        if self._history is not None and "Dividends" in self._history and "Stock Splits" in self._history:
-            action_columns = ["Dividends", "Stock Splits"]
-            if "Capital Gains" in self._history:
-                action_columns.append("Capital Gains")
-            actions = self._history[action_columns]
-            return actions[actions != 0].dropna(how='all').fillna(0)
-        return _empty_series
+        return self._lazy_load_price_history().get_actions(proxy)
 
     def get_shares(self, proxy=None, as_dict=False) -> Union[pd.DataFrame, dict]:
         self._fundamentals.proxy = proxy or self.proxy
@@ -2088,7 +503,7 @@ def get_news(self, proxy=None) -> list:
             return self._news
 
         # Getting data from json
-        url = f"{self._base_url}/v1/finance/search?q={self.ticker}"
+        url = f"{_BASE_URL_}/v1/finance/search?q={self.ticker}"
         data = self._data.cache_get(url=url, proxy=proxy)
         if "Will be right back" in data.text:
             raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
@@ -2191,12 +606,4 @@ def get_earnings_dates(self, limit=12, proxy=None) -> Optional[pd.DataFrame]:
         return dates
 
     def get_history_metadata(self, proxy=None) -> dict:
-        if self._history_metadata is None:
-            # Request intraday data, because then Yahoo returns exchange schedule.
-            self.history(period="1wk", interval="1h", prepost=True, proxy=proxy)
-
-        if self._history_metadata_formatted is False:
-            self._history_metadata = utils.format_history_metadata(self._history_metadata)
-            self._history_metadata_formatted = True
-
-        return self._history_metadata
+        return self._lazy_load_price_history().get_history_metadata(proxy)
diff --git a/yfinance/const.py b/yfinance/const.py
index 592737974..f4ec05a60 100644
--- a/yfinance/const.py
+++ b/yfinance/const.py
@@ -115,7 +115,7 @@
                   "PaymentstoSuppliersforGoodsandServices", "ClassesofCashReceiptsfromOperatingActivities",
                   "OtherCashReceiptsfromOperatingActivities", "ReceiptsfromGovernmentGrants", "ReceiptsfromCustomers"]}
 
-price_colnames = ['Open', 'High', 'Low', 'Close', 'Adj Close']
+_PRICE_COLNAMES_ = ['Open', 'High', 'Low', 'Close', 'Adj Close']
 
 quote_summary_valid_modules = (
     "summaryProfile",  # contains general information about the company
diff --git a/yfinance/scrapers/history.py b/yfinance/scrapers/history.py
new file mode 100644
index 000000000..0b3cf1f70
--- /dev/null
+++ b/yfinance/scrapers/history.py
@@ -0,0 +1,1630 @@
+
+import datetime as _datetime
+import dateutil as _dateutil
+import logging
+import numpy as np
+import pandas as pd
+import time as _time
+
+from yfinance import shared, utils
+from yfinance.const import _BASE_URL_, _PRICE_COLNAMES_
+
+class PriceHistory:
+    def __init__(self, data, ticker, tz, session=None, proxy=None):
+        self._data = data
+        self.ticker = ticker.upper()
+        self.tz = tz
+        self.proxy = proxy
+        self.session = session
+
+        self._history = None
+        self._history_metadata = None
+        self._history_metadata_formatted = False
+
+        # Limit recursion depth when repairing prices
+        self._reconstruct_start_interval = None
+        
+    @utils.log_indent_decorator
+    def history(self, period="1mo", interval="1d",
+                start=None, end=None, prepost=False, actions=True,
+                auto_adjust=True, back_adjust=False, repair=False, keepna=False,
+                proxy=None, rounding=False, timeout=10,
+                raise_errors=False) -> pd.DataFrame:
+        """
+        :Parameters:
+            period : str
+                Valid periods: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max
+                Either Use period parameter or use start and end
+            interval : str
+                Valid intervals: 1m,2m,5m,15m,30m,60m,90m,1h,1d,5d,1wk,1mo,3mo
+                Intraday data cannot extend last 60 days
+            start: str
+                Download start date string (YYYY-MM-DD) or _datetime, inclusive.
+                Default is 99 years ago
+                E.g. for start="2020-01-01", the first data point will be on "2020-01-01"
+            end: str
+                Download end date string (YYYY-MM-DD) or _datetime, exclusive.
+                Default is now
+                E.g. for end="2023-01-01", the last data point will be on "2022-12-31"
+            prepost : bool
+                Include Pre and Post market data in results?
+                Default is False
+            auto_adjust: bool
+                Adjust all OHLC automatically? Default is True
+            back_adjust: bool
+                Back-adjusted data to mimic true historical prices
+            repair: bool
+                Detect currency unit 100x mixups and attempt repair.
+                Default is False
+            keepna: bool
+                Keep NaN rows returned by Yahoo?
+                Default is False
+            proxy: str
+                Optional. Proxy server URL scheme. Default is None
+            rounding: bool
+                Round values to 2 decimal places?
+                Optional. Default is False = precision suggested by Yahoo!
+            timeout: None or float
+                If not None stops waiting for a response after given number of
+                seconds. (Can also be a fraction of a second e.g. 0.01)
+                Default is 10 seconds.
+            raise_errors: bool
+                If True, then raise errors as Exceptions instead of logging.
+        """
+        logger = utils.get_yf_logger()
+        proxy = proxy or self.proxy
+
+        start_user = start
+        end_user = end
+        if start or period is None or period.lower() == "max":
+            # Check can get TZ. Fail => probably delisted
+            tz = self.tz
+            if tz is None:
+                # Every valid ticker has a timezone. Missing = problem
+                err_msg = "No timezone found, symbol may be delisted"
+                shared._DFS[self.ticker] = utils.empty_df()
+                shared._ERRORS[self.ticker] = err_msg
+                if raise_errors:
+                    raise Exception(f'{self.ticker}: {err_msg}')
+                else:
+                    logger.error(f'{self.ticker}: {err_msg}')
+                return utils.empty_df()
+
+            if end is None:
+                end = int(_time.time())
+            else:
+                end = utils._parse_user_dt(end, tz)
+            if start is None:
+                if interval == "1m":
+                    start = end - 604800  # Subtract 7 days
+                else:
+                    max_start_datetime = pd.Timestamp.utcnow().floor("D") - _datetime.timedelta(days=99 * 365)
+                    start = int(max_start_datetime.timestamp())
+            else:
+                start = utils._parse_user_dt(start, tz)
+            params = {"period1": start, "period2": end}
+        else:
+            period = period.lower()
+            params = {"range": period}
+
+        params["interval"] = interval.lower()
+        params["includePrePost"] = prepost
+
+        # 1) fix weired bug with Yahoo! - returning 60m for 30m bars
+        if params["interval"] == "30m":
+            params["interval"] = "15m"
+
+        # if the ticker is MUTUALFUND or ETF, then get capitalGains events
+        params["events"] = "div,splits,capitalGains"
+
+        params_pretty = dict(params)
+        tz = self.tz
+        for k in ["period1", "period2"]:
+            if k in params_pretty:
+                params_pretty[k] = str(pd.Timestamp(params[k], unit='s').tz_localize("UTC").tz_convert(tz))
+        logger.debug(f'{self.ticker}: Yahoo GET parameters: {str(params_pretty)}')
+
+        # Getting data from json
+        url = f"{_BASE_URL_}/v8/finance/chart/{self.ticker}"
+        data = None
+        get_fn = self._data.get
+        if end is not None:
+            end_dt = pd.Timestamp(end, unit='s').tz_localize("UTC")
+            dt_now = pd.Timestamp.utcnow()
+            data_delay = _datetime.timedelta(minutes=30)
+            if end_dt + data_delay <= dt_now:
+                # Date range in past so safe to fetch through cache:
+                get_fn = self._data.cache_get
+        try:
+            data = get_fn(
+                url=url,
+                params=params,
+                proxy=proxy,
+                timeout=timeout
+            )
+            if "Will be right back" in data.text or data is None:
+                raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
+                                   "Our engineers are working quickly to resolve "
+                                   "the issue. Thank you for your patience.")
+
+            data = data.json()
+        except Exception:
+            if raise_errors:
+                raise
+
+        # Store the meta data that gets retrieved simultaneously
+        try:
+            self._history_metadata = data["chart"]["result"][0]["meta"]
+        except Exception:
+            self._history_metadata = {}
+
+        intraday = params["interval"][-1] in ("m", 'h')
+        err_msg = "No price data found, symbol may be delisted"
+        if start or period is None or period.lower() == "max":
+            err_msg += f' ({params["interval"]} '
+            if start_user is not None:
+                err_msg += f'{start_user}'
+            elif not intraday:
+                err_msg += f'{pd.Timestamp(start, unit="s").tz_localize("UTC").tz_convert(tz).date()}'
+            else:
+                err_msg += f'{pd.Timestamp(start, unit="s").tz_localize("UTC").tz_convert(tz)}'
+            err_msg += ' -> '
+            if end_user is not None:
+                err_msg += f'{end_user})'
+            elif not intraday:
+                err_msg += f'{pd.Timestamp(end, unit="s").tz_localize("UTC").tz_convert(tz).date()})'
+            else:
+                err_msg += f'{pd.Timestamp(end, unit="s").tz_localize("UTC").tz_convert(tz)})'
+        else:
+            err_msg += f' (period={period})'
+
+        fail = False
+        if data is None or not isinstance(data, dict):
+            fail = True
+        elif isinstance(data, dict) and 'status_code' in data:
+            err_msg += f"(Yahoo status_code = {data['status_code']})"
+            fail = True
+        elif "chart" in data and data["chart"]["error"]:
+            err_msg = data["chart"]["error"]["description"]
+            fail = True
+        elif "chart" not in data or data["chart"]["result"] is None or not data["chart"]["result"]:
+            fail = True
+        elif period is not None and "timestamp" not in data["chart"]["result"][0] and period not in \
+                self._history_metadata["validRanges"]:
+            # User provided a bad period. The minimum should be '1d', but sometimes Yahoo accepts '1h'.
+            err_msg = f"Period '{period}' is invalid, must be one of {self._history_metadata['validRanges']}"
+            fail = True
+        if fail:
+            shared._DFS[self.ticker] = utils.empty_df()
+            shared._ERRORS[self.ticker] = err_msg
+            if raise_errors:
+                raise Exception(f'{self.ticker}: {err_msg}')
+            else:
+                logger.error(f'{self.ticker}: {err_msg}')
+            if self._reconstruct_start_interval is not None and self._reconstruct_start_interval == interval:
+                self._reconstruct_start_interval = None
+            return utils.empty_df()
+
+        # parse quotes
+        try:
+            quotes = utils.parse_quotes(data["chart"]["result"][0])
+            # Yahoo bug fix - it often appends latest price even if after end date
+            if end and not quotes.empty:
+                endDt = pd.to_datetime(_datetime.datetime.utcfromtimestamp(end))
+                if quotes.index[quotes.shape[0] - 1] >= endDt:
+                    quotes = quotes.iloc[0:quotes.shape[0] - 1]
+        except Exception:
+            shared._DFS[self.ticker] = utils.empty_df()
+            shared._ERRORS[self.ticker] = err_msg
+            if raise_errors:
+                raise Exception(f'{self.ticker}: {err_msg}')
+            else:
+                logger.error(f'{self.ticker}: {err_msg}')
+            if self._reconstruct_start_interval is not None and self._reconstruct_start_interval == interval:
+                self._reconstruct_start_interval = None
+            return shared._DFS[self.ticker]
+        logger.debug(f'{self.ticker}: yfinance received OHLC data: {quotes.index[0]} -> {quotes.index[-1]}')
+
+        # 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')
+            quotes2 = quotes.resample('30T')
+            quotes = pd.DataFrame(index=quotes2.last().index, data={
+                'Open': quotes2['Open'].first(),
+                'High': quotes2['High'].max(),
+                'Low': quotes2['Low'].min(),
+                'Close': quotes2['Close'].last(),
+                'Adj Close': quotes2['Adj Close'].last(),
+                'Volume': quotes2['Volume'].sum()
+            })
+            try:
+                quotes['Dividends'] = quotes2['Dividends'].max()
+                quotes['Stock Splits'] = quotes2['Stock Splits'].max()
+            except Exception:
+                pass
+
+        # Select useful info from metadata
+        quote_type = self._history_metadata["instrumentType"]
+        expect_capital_gains = quote_type in ('MUTUALFUND', 'ETF')
+        tz_exchange = self._history_metadata["exchangeTimezoneName"]
+
+        # Note: ordering is important. If you change order, run the tests!
+        quotes = utils.set_df_tz(quotes, params["interval"], tz_exchange)
+        quotes = utils.fix_Yahoo_dst_issue(quotes, params["interval"])
+        quotes = utils.fix_Yahoo_returning_live_separate(quotes, params["interval"], tz_exchange)
+        intraday = params["interval"][-1] in ("m", 'h')
+        if not prepost and intraday and "tradingPeriods" in self._history_metadata:
+            tps = self._history_metadata["tradingPeriods"]
+            if not isinstance(tps, pd.DataFrame):
+                self._history_metadata = utils.format_history_metadata(self._history_metadata, tradingPeriodsOnly=True)
+                tps = self._history_metadata["tradingPeriods"]
+            quotes = utils.fix_Yahoo_returning_prepost_unrequested(quotes, params["interval"], tps)
+        logger.debug(f'{self.ticker}: OHLC after cleaning: {quotes.index[0]} -> {quotes.index[-1]}')
+
+        # actions
+        dividends, splits, capital_gains = utils.parse_actions(data["chart"]["result"][0])
+        if not expect_capital_gains:
+            capital_gains = None
+
+        if splits is not None:
+            splits = utils.set_df_tz(splits, interval, tz_exchange)
+        if dividends is not None:
+            dividends = utils.set_df_tz(dividends, interval, tz_exchange)
+        if capital_gains is not None:
+            capital_gains = utils.set_df_tz(capital_gains, interval, tz_exchange)
+        if start is not None:
+            if not quotes.empty:
+                startDt = quotes.index[0].floor('D')
+                if dividends is not None:
+                    dividends = dividends.loc[startDt:]
+                if capital_gains is not None:
+                    capital_gains = capital_gains.loc[startDt:]
+                if splits is not None:
+                    splits = splits.loc[startDt:]
+        if end is not None:
+            endDt = pd.Timestamp(end, unit='s').tz_localize(tz)
+            if dividends is not None:
+                dividends = dividends[dividends.index < endDt]
+            if capital_gains is not None:
+                capital_gains = capital_gains[capital_gains.index < endDt]
+            if splits is not None:
+                splits = splits[splits.index < endDt]
+
+        # Prepare for combine
+        intraday = params["interval"][-1] in ("m", 'h')
+        if not intraday:
+            # If localizing a midnight during DST transition hour when clocks roll back,
+            # meaning clock hits midnight twice, then use the 2nd (ambiguous=True)
+            quotes.index = pd.to_datetime(quotes.index.date).tz_localize(tz_exchange, ambiguous=True, nonexistent='shift_forward')
+            if dividends.shape[0] > 0:
+                dividends.index = pd.to_datetime(dividends.index.date).tz_localize(tz_exchange, ambiguous=True, nonexistent='shift_forward')
+            if splits.shape[0] > 0:
+                splits.index = pd.to_datetime(splits.index.date).tz_localize(tz_exchange, ambiguous=True, nonexistent='shift_forward')
+
+        # Combine
+        df = quotes.sort_index()
+        if dividends.shape[0] > 0:
+            df = utils.safe_merge_dfs(df, dividends, interval)
+        if "Dividends" in df.columns:
+            df.loc[df["Dividends"].isna(), "Dividends"] = 0
+        else:
+            df["Dividends"] = 0.0
+        if splits.shape[0] > 0:
+            df = utils.safe_merge_dfs(df, splits, interval)
+        if "Stock Splits" in df.columns:
+            df.loc[df["Stock Splits"].isna(), "Stock Splits"] = 0
+        else:
+            df["Stock Splits"] = 0.0
+        if expect_capital_gains:
+            if capital_gains.shape[0] > 0:
+                df = utils.safe_merge_dfs(df, capital_gains, interval)
+            if "Capital Gains" in df.columns:
+                df.loc[df["Capital Gains"].isna(), "Capital Gains"] = 0
+            else:
+                df["Capital Gains"] = 0.0
+        logger.debug(f'{self.ticker}: OHLC after combining events: {quotes.index[0]} -> {quotes.index[-1]}')
+
+        df = df[~df.index.duplicated(keep='first')]  # must do before repair
+
+        if repair:
+            # Do this before auto/back adjust
+            logger.debug(f'{self.ticker}: checking OHLC for repairs ...')
+            df = self._fix_unit_mixups(df, interval, tz_exchange, prepost)
+            df = self._fix_bad_stock_split(df, interval, tz_exchange)
+            # Must repair 100x and split errors before price reconstruction
+            df = self._fix_zeroes(df, interval, tz_exchange, prepost)
+            df = self._fix_missing_div_adjust(df, interval, tz_exchange)
+            df = df.sort_index()
+
+        # Auto/back adjust
+        try:
+            if auto_adjust:
+                df = utils.auto_adjust(df)
+            elif back_adjust:
+                df = utils.back_adjust(df)
+        except Exception as e:
+            if auto_adjust:
+                err_msg = "auto_adjust failed with %s" % e
+            else:
+                err_msg = "back_adjust failed with %s" % e
+            shared._DFS[self.ticker] = utils.empty_df()
+            shared._ERRORS[self.ticker] = err_msg
+            if raise_errors:
+                raise Exception('%s: %s' % (self.ticker, err_msg))
+            else:
+                logger.error('%s: %s' % (self.ticker, err_msg))
+
+        if rounding:
+            df = np.round(df, data["chart"]["result"][0]["meta"]["priceHint"])
+        df['Volume'] = df['Volume'].fillna(0).astype(np.int64)
+
+        if intraday:
+            df.index.name = "Datetime"
+        else:
+            df.index.name = "Date"
+
+        self._history = df.copy()
+
+        # missing rows cleanup
+        if not actions:
+            df = df.drop(columns=["Dividends", "Stock Splits", "Capital Gains"], errors='ignore')
+        if not keepna:
+            data_colnames = _PRICE_COLNAMES_ + ['Volume'] + ['Dividends', 'Stock Splits', 'Capital Gains']
+            data_colnames = [c for c in data_colnames if c in df.columns]
+            mask_nan_or_zero = (df[data_colnames].isna() | (df[data_colnames] == 0)).all(axis=1)
+            df = df.drop(mask_nan_or_zero.index[mask_nan_or_zero])
+
+        logger.debug(f'{self.ticker}: yfinance returning OHLC: {df.index[0]} -> {df.index[-1]}')
+
+        if self._reconstruct_start_interval is not None and self._reconstruct_start_interval == interval:
+            self._reconstruct_start_interval = None
+        return df
+
+    def get_history_metadata(self, proxy=None) -> dict:
+        if self._history_metadata is None:
+            # Request intraday data, because then Yahoo returns exchange schedule.
+            self.history(period="1wk", interval="1h", prepost=True, proxy=proxy)
+
+        if self._history_metadata_formatted is False:
+            self._history_metadata = utils.format_history_metadata(self._history_metadata)
+            self._history_metadata_formatted = True
+
+        return self._history_metadata
+
+    def get_dividends(self, proxy=None) -> pd.Series:
+        if self._history is None:
+            self.history(period="max", proxy=proxy)
+        if self._history is not None and "Dividends" in self._history:
+            dividends = self._history["Dividends"]
+            return dividends[dividends != 0]
+        return pd.Series()
+
+    def get_capital_gains(self, proxy=None) -> pd.Series:
+        if self._history is None:
+            self.history(period="max", proxy=proxy)
+        if self._history is not None and "Capital Gains" in self._history:
+            capital_gains = self._history["Capital Gains"]
+            return capital_gains[capital_gains != 0]
+        return pd.Series()
+
+    def get_splits(self, proxy=None) -> pd.Series:
+        if self._history is None:
+            self.history(period="max", proxy=proxy)
+        if self._history is not None and "Stock Splits" in self._history:
+            splits = self._history["Stock Splits"]
+            return splits[splits != 0]
+        return pd.Series()
+
+    def get_actions(self, proxy=None) -> pd.Series:
+        if self._history is None:
+            self.history(period="max", proxy=proxy)
+        if self._history is not None and "Dividends" in self._history and "Stock Splits" in self._history:
+            action_columns = ["Dividends", "Stock Splits"]
+            if "Capital Gains" in self._history:
+                action_columns.append("Capital Gains")
+            actions = self._history[action_columns]
+            return actions[actions != 0].dropna(how='all').fillna(0)
+        return pd.Series()
+
+    @utils.log_indent_decorator
+    def _reconstruct_intervals_batch(self, df, interval, prepost, tag=-1):
+        # Reconstruct values in df using finer-grained price data. Delimiter marks what to reconstruct
+        logger = utils.get_yf_logger()
+
+        if not isinstance(df, pd.DataFrame):
+            raise Exception("'df' must be a Pandas DataFrame not", type(df))
+        if interval == "1m":
+            # Can't go smaller than 1m so can't reconstruct
+            return df
+
+        if interval[1:] in ['d', 'wk', 'mo']:
+            # Interday data always includes pre & post
+            prepost = True
+            intraday = False
+        else:
+            intraday = True
+
+        price_cols = [c for c in _PRICE_COLNAMES_ if c in df]
+        data_cols = price_cols + ["Volume"]
+
+        # If interval is weekly then can construct with daily. But if smaller intervals then
+        # restricted to recent times:
+        intervals = ["1wk", "1d", "1h", "30m", "15m", "5m", "2m", "1m"]
+        itds = {i: utils._interval_to_timedelta(interval) for i in intervals}
+        nexts = {intervals[i]: intervals[i + 1] for i in range(len(intervals) - 1)}
+        min_lookbacks = {"1wk": None, "1d": None, "1h": _datetime.timedelta(days=730)}
+        for i in ["30m", "15m", "5m", "2m"]:
+            min_lookbacks[i] = _datetime.timedelta(days=60)
+        min_lookbacks["1m"] = _datetime.timedelta(days=30)
+        if interval in nexts:
+            sub_interval = nexts[interval]
+            td_range = itds[interval]
+        else:
+            logger.warning(f"Have not implemented price repair for '{interval}' interval. Contact developers")
+            if "Repaired?" not in df.columns:
+                df["Repaired?"] = False
+            return df
+
+        # Limit max reconstruction depth to 2:
+        if self._reconstruct_start_interval is None:
+            self._reconstruct_start_interval = interval
+        if interval != self._reconstruct_start_interval and interval != nexts[self._reconstruct_start_interval]:
+            logger.debug(f"{self.ticker}: Price repair has hit max depth of 2 ('%s'->'%s'->'%s')", self._reconstruct_start_interval, nexts[self._reconstruct_start_interval], interval)
+            return df
+
+        df = df.sort_index()
+
+        f_repair = df[data_cols].to_numpy() == tag
+        f_repair_rows = f_repair.any(axis=1)
+
+        # Ignore old intervals for which Yahoo won't return finer data:
+        m = min_lookbacks[sub_interval]
+        if m is None:
+            min_dt = None
+        else:
+            m -= _datetime.timedelta(days=1)  # allow space for 1-day padding
+            min_dt = pd.Timestamp.utcnow() - m
+            min_dt = min_dt.tz_convert(df.index.tz).ceil("D")
+        logger.debug(f"min_dt={min_dt} interval={interval} sub_interval={sub_interval}")
+        if min_dt is not None:
+            f_recent = df.index >= min_dt
+            f_repair_rows = f_repair_rows & f_recent
+            if not f_repair_rows.any():
+                logger.info("Data too old to repair")
+                if "Repaired?" not in df.columns:
+                    df["Repaired?"] = False
+                return df
+
+        dts_to_repair = df.index[f_repair_rows]
+
+        if len(dts_to_repair) == 0:
+            logger.info("Nothing needs repairing (dts_to_repair[] empty)")
+            if "Repaired?" not in df.columns:
+                df["Repaired?"] = False
+            return df
+
+        df_v2 = df.copy()
+        if "Repaired?" not in df_v2.columns:
+            df_v2["Repaired?"] = False
+        f_good = ~(df[price_cols].isna().any(axis=1))
+        f_good = f_good & (df[price_cols].to_numpy() != tag).all(axis=1)
+        df_good = df[f_good]
+
+        # Group nearby NaN-intervals together to reduce number of Yahoo fetches
+        dts_groups = [[dts_to_repair[0]]]
+        # Note on setting max size: have to allow space for adding good data
+        if sub_interval == "1mo":
+            grp_max_size = _dateutil.relativedelta.relativedelta(years=2)
+        elif sub_interval == "1wk":
+            grp_max_size = _dateutil.relativedelta.relativedelta(years=2)
+        elif sub_interval == "1d":
+            grp_max_size = _dateutil.relativedelta.relativedelta(years=2)
+        elif sub_interval == "1h":
+            grp_max_size = _dateutil.relativedelta.relativedelta(years=1)
+        elif sub_interval == "1m":
+            grp_max_size = _datetime.timedelta(days=5)  # allow 2 days for buffer below
+        else:
+            grp_max_size = _datetime.timedelta(days=30)
+        logger.debug(f"grp_max_size = {grp_max_size}")
+        for i in range(1, len(dts_to_repair)):
+            dt = dts_to_repair[i]
+            if dt.date() < dts_groups[-1][0].date() + grp_max_size:
+                dts_groups[-1].append(dt)
+            else:
+                dts_groups.append([dt])
+
+        logger.debug("Repair groups:")
+        for g in dts_groups:
+            logger.debug(f"- {g[0]} -> {g[-1]}")
+
+        # Add some good data to each group, so can calibrate prices later:
+        for i in range(len(dts_groups)):
+            g = dts_groups[i]
+            g0 = g[0]
+            i0 = df_good.index.get_indexer([g0], method="nearest")[0]
+            if i0 > 0:
+                if (min_dt is None or df_good.index[i0 - 1] >= min_dt) and \
+                        ((not intraday) or df_good.index[i0 - 1].date() == g0.date()):
+                    i0 -= 1
+            gl = g[-1]
+            il = df_good.index.get_indexer([gl], method="nearest")[0]
+            if il < len(df_good) - 1:
+                if (not intraday) or df_good.index[il + 1].date() == gl.date():
+                    il += 1
+            good_dts = df_good.index[i0:il + 1]
+            dts_groups[i] += good_dts.to_list()
+            dts_groups[i].sort()
+
+        n_fixed = 0
+        for g in dts_groups:
+            df_block = df[df.index.isin(g)]
+            logger.debug("df_block:\n" + str(df_block))
+
+            start_dt = g[0]
+            start_d = start_dt.date()
+            reject = False
+            if sub_interval == "1h" and (_datetime.date.today() - start_d) > _datetime.timedelta(days=729):
+                reject = True
+            elif sub_interval in ["30m", "15m"] and (_datetime.date.today() - start_d) > _datetime.timedelta(days=59):
+                reject = True
+            if reject:
+                # Don't bother requesting more price data, Yahoo will reject
+                msg = f"Cannot reconstruct {interval} block starting"
+                if intraday:
+                    msg += f" {start_dt}"
+                else:
+                    msg += f" {start_d}"
+                msg += ", too old, Yahoo will reject request for finer-grain data"
+                logger.info(msg)
+                continue
+
+            td_1d = _datetime.timedelta(days=1)
+            end_dt = g[-1]
+            end_d = end_dt.date() + td_1d
+            if interval in "1wk":
+                fetch_start = start_d - td_range  # need previous week too
+                fetch_end = g[-1].date() + td_range
+            elif interval == "1d":
+                fetch_start = start_d
+                fetch_end = g[-1].date() + td_range
+            else:
+                fetch_start = g[0]
+                fetch_end = g[-1] + td_range
+
+            # The first and last day returned by Yahoo can be slightly wrong, so add buffer:
+            fetch_start -= td_1d
+            fetch_end += td_1d
+            if intraday:
+                fetch_start = fetch_start.date()
+                fetch_end = fetch_end.date() + td_1d
+            if min_dt is not None:
+                fetch_start = max(min_dt.date(), fetch_start)
+            logger.debug(f"Fetching {sub_interval} prepost={prepost} {fetch_start}->{fetch_end}")
+            df_fine = self.history(start=fetch_start, end=fetch_end, interval=sub_interval, auto_adjust=False, actions=True, prepost=prepost, repair=True, keepna=True)
+            if df_fine is None or df_fine.empty:
+                msg = f"Cannot reconstruct {interval} block starting"
+                if intraday:
+                    msg += f" {start_dt}"
+                else:
+                    msg += f" {start_d}"
+                msg += ", too old, Yahoo is rejecting request for finer-grain data"
+                logger.debug(msg)
+                continue
+            # Discard the buffer
+            df_fine = df_fine.loc[g[0]: g[-1] + itds[sub_interval] - _datetime.timedelta(milliseconds=1)].copy()
+            if df_fine.empty:
+                msg = f"Cannot reconstruct {interval} block range"
+                if intraday:
+                    msg += f" {start_dt}->{end_dt}"
+                else:
+                    msg += f" {start_d}->{end_d}"
+                msg += ", Yahoo not returning finer-grain data within range"
+                logger.debug(msg)
+                continue
+
+            df_fine["ctr"] = 0
+            if interval == "1wk":
+                weekdays = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]
+                week_end_day = weekdays[(df_block.index[0].weekday() + 7 - 1) % 7]
+                df_fine["Week Start"] = df_fine.index.tz_localize(None).to_period("W-" + week_end_day).start_time
+                grp_col = "Week Start"
+            elif interval == "1d":
+                df_fine["Day Start"] = pd.to_datetime(df_fine.index.date)
+                grp_col = "Day Start"
+            else:
+                df_fine.loc[df_fine.index.isin(df_block.index), "ctr"] = 1
+                df_fine["intervalID"] = df_fine["ctr"].cumsum()
+                df_fine = df_fine.drop("ctr", axis=1)
+                grp_col = "intervalID"
+            df_fine = df_fine[~df_fine[price_cols + ['Dividends']].isna().all(axis=1)]
+
+            df_fine_grp = df_fine.groupby(grp_col)
+            df_new = df_fine_grp.agg(
+                Open=("Open", "first"),
+                Close=("Close", "last"),
+                AdjClose=("Adj Close", "last"),
+                Low=("Low", "min"),
+                High=("High", "max"),
+                Dividends=("Dividends", "sum"),
+                Volume=("Volume", "sum")).rename(columns={"AdjClose": "Adj Close"})
+            if grp_col in ["Week Start", "Day Start"]:
+                df_new.index = df_new.index.tz_localize(df_fine.index.tz)
+            else:
+                df_fine["diff"] = df_fine["intervalID"].diff()
+                new_index = np.append([df_fine.index[0]], df_fine.index[df_fine["intervalID"].diff() > 0])
+                df_new.index = new_index
+            logger.debug('df_new:' + '\n' + str(df_new))
+
+            # Calibrate!
+            common_index = np.intersect1d(df_block.index, df_new.index)
+            if len(common_index) == 0:
+                # Can't calibrate so don't attempt repair
+                logger.info(f"Can't calibrate {interval} block starting {start_d} so aborting repair")
+                continue
+            # First, attempt to calibrate the 'Adj Close' column. OK if cannot.
+            # Only necessary for 1d interval, because the 1h data is not div-adjusted.
+            if interval == '1d':
+                df_new_calib = df_new[df_new.index.isin(common_index)]
+                df_block_calib = df_block[df_block.index.isin(common_index)]
+                f_tag = df_block_calib['Adj Close'] == tag
+                if f_tag.any():
+                    div_adjusts = df_block_calib['Adj Close'] / df_block_calib['Close']
+                    # The loop below assumes each 1d repair is isolated, i.e. surrounded by
+                    # good data. Which is case most of time.
+                    # But in case are repairing a chunk of bad 1d data, back/forward-fill the
+                    # good div-adjustments - not perfect, but a good backup.
+                    div_adjusts[f_tag] = np.nan
+                    div_adjusts = div_adjusts.ffill().bfill()
+                    for idx in np.where(f_tag)[0]:
+                        dt = df_new_calib.index[idx]
+                        n = len(div_adjusts)
+                        if df_new.loc[dt, "Dividends"] != 0:
+                            if idx < n - 1:
+                                # Easy, take div-adjustment from next-day
+                                div_adjusts.iloc[idx] = div_adjusts.iloc[idx + 1]
+                            else:
+                                # Take previous-day div-adjustment and reverse todays adjustment
+                                div_adj = 1.0 - df_new_calib["Dividends"].iloc[idx] / df_new_calib['Close'].iloc[
+                                    idx - 1]
+                                div_adjusts.iloc[idx] = div_adjusts.iloc[idx - 1] / div_adj
+                        else:
+                            if idx > 0:
+                                # Easy, take div-adjustment from previous-day
+                                div_adjusts.iloc[idx] = div_adjusts.iloc[idx - 1]
+                            else:
+                                # Must take next-day div-adjustment
+                                div_adjusts.iloc[idx] = div_adjusts.iloc[idx + 1]
+                                if df_new_calib["Dividends"].iloc[idx + 1] != 0:
+                                    div_adjusts.iloc[idx] *= 1.0 - df_new_calib["Dividends"].iloc[idx + 1] / \
+                                                        df_new_calib['Close'].iloc[idx]
+                    f_close_bad = df_block_calib['Close'] == tag
+                    div_adjusts = div_adjusts.reindex(df_block.index, fill_value=np.nan).ffill().bfill()
+                    df_new['Adj Close'] = df_block['Close'] * div_adjusts
+                    if f_close_bad.any():
+                        f_close_bad_new = f_close_bad.reindex(df_new.index, fill_value=False)
+                        div_adjusts_new = div_adjusts.reindex(df_new.index, fill_value=np.nan).ffill().bfill()
+                        div_adjusts_new_np = f_close_bad_new.to_numpy()
+                        df_new.loc[div_adjusts_new_np, 'Adj Close'] = df_new['Close'][div_adjusts_new_np] * div_adjusts_new[div_adjusts_new_np]
+
+            # Check whether 'df_fine' has different split-adjustment.
+            # If different, then adjust to match 'df'
+            calib_cols = ['Open', 'Close']
+            df_new_calib = df_new[df_new.index.isin(common_index)][calib_cols].to_numpy()
+            df_block_calib = df_block[df_block.index.isin(common_index)][calib_cols].to_numpy()
+            calib_filter = (df_block_calib != tag)
+            if not calib_filter.any():
+                # Can't calibrate so don't attempt repair
+                logger.info(f"Can't calibrate {interval} block starting {start_d} so aborting repair")
+                continue
+            # Avoid divide-by-zero warnings:
+            for j in range(len(calib_cols)):
+                f = ~calib_filter[:, j]
+                if f.any():
+                    df_block_calib[f, j] = 1
+                    df_new_calib[f, j] = 1
+            ratios = df_block_calib[calib_filter] / df_new_calib[calib_filter]
+            weights = df_fine_grp.size()
+            weights.index = df_new.index
+            weights = weights[weights.index.isin(common_index)].to_numpy().astype(float)
+            weights = weights[:, None]  # transpose
+            weights = np.tile(weights, len(calib_cols))  # 1D -> 2D
+            weights = weights[calib_filter]  # flatten
+            not1 = ~np.isclose(ratios, 1.0, rtol=0.00001)
+            if np.sum(not1) == len(calib_cols):
+                # Only 1 calibration row in df_new is different to df_block so ignore
+                ratio = 1.0
+            else:
+                ratio = np.average(ratios, weights=weights)
+            logger.debug(f"Price calibration ratio (raw) = {ratio:6f}")
+            ratio_rcp = round(1.0 / ratio, 1)
+            ratio = round(ratio, 1)
+            if ratio == 1 and ratio_rcp == 1:
+                # Good!
+                pass
+            else:
+                if ratio > 1:
+                    # data has different split-adjustment than fine-grained data
+                    # Adjust fine-grained to match
+                    df_new[price_cols] *= ratio
+                    df_new["Volume"] /= ratio
+                elif ratio_rcp > 1:
+                    # data has different split-adjustment than fine-grained data
+                    # Adjust fine-grained to match
+                    df_new[price_cols] *= 1.0 / ratio_rcp
+                    df_new["Volume"] *= ratio_rcp
+
+            # Repair!
+            bad_dts = df_block.index[(df_block[price_cols + ["Volume"]] == tag).to_numpy().any(axis=1)]
+
+            no_fine_data_dts = []
+            for idx in bad_dts:
+                if idx not in df_new.index:
+                    # Yahoo didn't return finer-grain data for this interval,
+                    # so probably no trading happened.
+                    no_fine_data_dts.append(idx)
+            if len(no_fine_data_dts) > 0:
+                logger.debug("Yahoo didn't return finer-grain data for these intervals: " + str(no_fine_data_dts))
+            for idx in bad_dts:
+                if idx not in df_new.index:
+                    # Yahoo didn't return finer-grain data for this interval,
+                    # so probably no trading happened.
+                    continue
+                df_new_row = df_new.loc[idx]
+
+                if interval == "1wk":
+                    df_last_week = df_new.iloc[df_new.index.get_loc(idx) - 1]
+                    df_fine = df_fine.loc[idx:]
+
+                df_bad_row = df.loc[idx]
+                bad_fields = df_bad_row.index[df_bad_row == tag].to_numpy()
+                if "High" in bad_fields:
+                    df_v2.loc[idx, "High"] = df_new_row["High"]
+                if "Low" in bad_fields:
+                    df_v2.loc[idx, "Low"] = df_new_row["Low"]
+                if "Open" in bad_fields:
+                    if interval == "1wk" and idx != df_fine.index[0]:
+                        # Exchange closed Monday. In this case, Yahoo sets Open to last week close
+                        df_v2.loc[idx, "Open"] = df_last_week["Close"]
+                        df_v2.loc[idx, "Low"] = min(df_v2.loc[idx, "Open"], df_v2.loc[idx, "Low"])
+                    else:
+                        df_v2.loc[idx, "Open"] = df_new_row["Open"]
+                if "Close" in bad_fields:
+                    df_v2.loc[idx, "Close"] = df_new_row["Close"]
+                    # Assume 'Adj Close' also corrupted, easier than detecting whether true
+                    df_v2.loc[idx, "Adj Close"] = df_new_row["Adj Close"]
+                elif "Adj Close" in bad_fields:
+                    df_v2.loc[idx, "Adj Close"] = df_new_row["Adj Close"]
+                if "Volume" in bad_fields:
+                    df_v2.loc[idx, "Volume"] = df_new_row["Volume"]
+                df_v2.loc[idx, "Repaired?"] = True
+                n_fixed += 1
+
+        return df_v2
+
+    @utils.log_indent_decorator
+    def _fix_unit_mixups(self, df, interval, tz_exchange, prepost):
+        if df.empty:
+            return df
+        df2 = self._fix_unit_switch(df, interval, tz_exchange)
+        df3 = self._fix_unit_random_mixups(df2, interval, tz_exchange, prepost)
+        return df3
+
+    @utils.log_indent_decorator
+    def _fix_unit_random_mixups(self, df, interval, tz_exchange, prepost):
+        # Sometimes Yahoo returns few prices in cents/pence instead of $/£
+        # I.e. 100x bigger
+        # 2 ways this manifests:
+        # - random 100x errors spread throughout table
+        # - a sudden switch between $<->cents at some date
+        # This function fixes the first.
+
+        if df.empty:
+            return df
+
+        # Easy to detect and fix, just look for outliers = ~100x local median
+        logger = utils.get_yf_logger()
+
+        if df.shape[0] == 0:
+            if "Repaired?" not in df.columns:
+                df["Repaired?"] = False
+            return df
+        if df.shape[0] == 1:
+            # Need multiple rows to confidently identify outliers
+            logger.info("price-repair-100x: Cannot check single-row table for 100x price errors")
+            if "Repaired?" not in df.columns:
+                df["Repaired?"] = False
+            return df
+
+        df2 = df.copy()
+
+        if df2.index.tz is None:
+            df2.index = df2.index.tz_localize(tz_exchange)
+        elif df2.index.tz != tz_exchange:
+            df2.index = df2.index.tz_convert(tz_exchange)
+
+        # Only import scipy if users actually want function. To avoid
+        # adding it to dependencies.
+        from scipy import ndimage as _ndimage
+
+        data_cols = ["High", "Open", "Low", "Close", "Adj Close"]  # Order important, separate High from Low
+        data_cols = [c for c in data_cols if c in df2.columns]
+        f_zeroes = (df2[data_cols] == 0).any(axis=1).to_numpy()
+        if f_zeroes.any():
+            df2_zeroes = df2[f_zeroes]
+            df2 = df2[~f_zeroes]
+            df = df[~f_zeroes]  # all row slicing must be applied to both df and df2
+        else:
+            df2_zeroes = None
+        if df2.shape[0] <= 1:
+            logger.info("price-repair-100x: Insufficient good data for detecting 100x price errors")
+            if "Repaired?" not in df.columns:
+                df["Repaired?"] = False
+            return df
+        df2_data = df2[data_cols].to_numpy()
+        median = _ndimage.median_filter(df2_data, size=(3, 3), mode="wrap")
+        ratio = df2_data / median
+        ratio_rounded = (ratio / 20).round() * 20  # round ratio to nearest 20
+        f = ratio_rounded == 100
+        ratio_rcp = 1.0/ratio
+        ratio_rcp_rounded = (ratio_rcp / 20).round() * 20  # round ratio to nearest 20
+        f_rcp = (ratio_rounded == 100) | (ratio_rcp_rounded == 100)
+        f_either = f | f_rcp
+        if not f_either.any():
+            logger.info("price-repair-100x: No sporadic 100x errors")
+            if "Repaired?" not in df.columns:
+                df["Repaired?"] = False
+            return df
+
+        # Mark values to send for repair
+        tag = -1.0
+        for i in range(len(data_cols)):
+            fi = f_either[:, i]
+            c = data_cols[i]
+            df2.loc[fi, c] = tag
+
+        n_before = (df2_data == tag).sum()
+        df2 = self._reconstruct_intervals_batch(df2, interval, prepost, tag)
+        df2_tagged = df2[data_cols].to_numpy() == tag
+        n_after = (df2[data_cols].to_numpy() == tag).sum()
+
+        if n_after > 0:
+            # This second pass will *crudely* "fix" any remaining errors in High/Low
+            # simply by ensuring they don't contradict e.g. Low = 100x High.
+            f = (df2[data_cols].to_numpy() == tag) & f
+            for i in range(f.shape[0]):
+                fi = f[i, :]
+                if not fi.any():
+                    continue
+                idx = df2.index[i]
+
+                for c in ['Open', 'Close']:
+                    j = data_cols.index(c)
+                    if fi[j]:
+                        df2.loc[idx, c] = df.loc[idx, c] * 0.01
+
+                c = "High"
+                j = data_cols.index(c)
+                if fi[j]:
+                    df2.loc[idx, c] = df2.loc[idx, ["Open", "Close"]].max()
+
+                c = "Low"
+                j = data_cols.index(c)
+                if fi[j]:
+                    df2.loc[idx, c] = df2.loc[idx, ["Open", "Close"]].min()
+
+            f_rcp = (df2[data_cols].to_numpy() == tag) & f_rcp
+            for i in range(f_rcp.shape[0]):
+                fi = f_rcp[i, :]
+                if not fi.any():
+                    continue
+                idx = df2.index[i]
+
+                for c in ['Open', 'Close']:
+                    j = data_cols.index(c)
+                    if fi[j]:
+                        df2.loc[idx, c] = df.loc[idx, c] * 100.0
+
+                c = "High"
+                j = data_cols.index(c)
+                if fi[j]:
+                    df2.loc[idx, c] = df2.loc[idx, ["Open", "Close"]].max()
+
+                c = "Low"
+                j = data_cols.index(c)
+                if fi[j]:
+                    df2.loc[idx, c] = df2.loc[idx, ["Open", "Close"]].min()
+
+            df2_tagged = df2[data_cols].to_numpy() == tag
+            n_after_crude = df2_tagged.sum()
+        else:
+            n_after_crude = n_after
+
+        n_fixed = n_before - n_after_crude
+        n_fixed_crudely = n_after - n_after_crude
+        if n_fixed > 0:
+            report_msg = f"{self.ticker}: fixed {n_fixed}/{n_before} currency unit mixups "
+            if n_fixed_crudely > 0:
+                report_msg += f"({n_fixed_crudely} crudely) "
+            report_msg += f"in {interval} price data"
+            logger.info('price-repair-100x: ' + report_msg)
+
+        # Restore original values where repair failed
+        f_either = df2[data_cols].to_numpy() == tag
+        for j in range(len(data_cols)):
+            fj = f_either[:, j]
+            if fj.any():
+                c = data_cols[j]
+                df2.loc[fj, c] = df.loc[fj, c]
+        if df2_zeroes is not None:
+            if "Repaired?" not in df2_zeroes.columns:
+                df2_zeroes["Repaired?"] = False
+            df2 = pd.concat([df2, df2_zeroes]).sort_index()
+            df2.index = pd.to_datetime(df2.index)
+
+        return df2
+
+    @utils.log_indent_decorator
+    def _fix_unit_switch(self, df, interval, tz_exchange):
+        # Sometimes Yahoo returns few prices in cents/pence instead of $/£
+        # I.e. 100x bigger
+        # 2 ways this manifests:
+        # - random 100x errors spread throughout table
+        # - a sudden switch between $<->cents at some date
+        # This function fixes the second.
+        # Eventually Yahoo fixes but could take them 2 weeks.
+
+        return self._fix_prices_sudden_change(df, interval, tz_exchange, 100.0)
+
+    @utils.log_indent_decorator
+    def _fix_zeroes(self, df, interval, tz_exchange, prepost):
+        # Sometimes Yahoo returns prices=0 or NaN when trades occurred.
+        # But most times when prices=0 or NaN returned is because no trades.
+        # Impossible to distinguish, so only attempt repair if few or rare.
+
+        if df.empty:
+            return df
+
+        logger = utils.get_yf_logger()
+
+        if df.shape[0] == 0:
+            if "Repaired?" not in df.columns:
+                df["Repaired?"] = False
+            return df
+
+        intraday = interval[-1] in ("m", 'h')
+
+        df = df.sort_index()  # important!
+        df2 = df.copy()
+
+        if df2.index.tz is None:
+            df2.index = df2.index.tz_localize(tz_exchange)
+        elif df2.index.tz != tz_exchange:
+            df2.index = df2.index.tz_convert(tz_exchange)
+
+        price_cols = [c for c in _PRICE_COLNAMES_ if c in df2.columns]
+        f_prices_bad = (df2[price_cols] == 0.0) | df2[price_cols].isna()
+        df2_reserve = None
+        if intraday:
+            # Ignore days with >50% intervals containing NaNs
+            grp = pd.Series(f_prices_bad.any(axis=1), name="nan").groupby(f_prices_bad.index.date)
+            nan_pct = grp.sum() / grp.count()
+            dts = nan_pct.index[nan_pct > 0.5]
+            f_zero_or_nan_ignore = np.isin(f_prices_bad.index.date, dts)
+            df2_reserve = df2[f_zero_or_nan_ignore]
+            df2 = df2[~f_zero_or_nan_ignore]
+            f_prices_bad = (df2[price_cols] == 0.0) | df2[price_cols].isna()
+
+        f_high_low_good = (~df2["High"].isna().to_numpy()) & (~df2["Low"].isna().to_numpy())
+        f_change = df2["High"].to_numpy() != df2["Low"].to_numpy()
+        f_vol_bad = (df2["Volume"] == 0).to_numpy() & f_high_low_good & f_change
+
+        # If stock split occurred, then trading must have happened.
+        # I should probably rename the function, because prices aren't zero ...
+        if 'Stock Splits' in df2.columns:
+            f_split = (df2['Stock Splits'] != 0.0).to_numpy()
+            if f_split.any():
+                f_change_expected_but_missing = f_split & ~f_change
+                if f_change_expected_but_missing.any():
+                    f_prices_bad[f_change_expected_but_missing] = True
+
+        # Check whether worth attempting repair
+        f_prices_bad = f_prices_bad.to_numpy()
+        f_bad_rows = f_prices_bad.any(axis=1) | f_vol_bad
+        if not f_bad_rows.any():
+            logger.info("price-repair-missing: No price=0 errors to repair")
+            if "Repaired?" not in df.columns:
+                df["Repaired?"] = False
+            return df
+        if f_prices_bad.sum() == len(price_cols) * len(df2):
+            # Need some good data to calibrate
+            logger.info("price-repair-missing: No good data for calibration so cannot fix price=0 bad data")
+            if "Repaired?" not in df.columns:
+                df["Repaired?"] = False
+            return df
+
+        data_cols = price_cols + ["Volume"]
+
+        # Mark values to send for repair
+        tag = -1.0
+        for i in range(len(price_cols)):
+            c = price_cols[i]
+            df2.loc[f_prices_bad[:, i], c] = tag
+        df2.loc[f_vol_bad, "Volume"] = tag
+        # If volume=0 or NaN for bad prices, then tag volume for repair
+        f_vol_zero_or_nan = (df2["Volume"].to_numpy() == 0) | (df2["Volume"].isna().to_numpy())
+        df2.loc[f_prices_bad.any(axis=1) & f_vol_zero_or_nan, "Volume"] = tag
+        # If volume=0 or NaN but price moved in interval, then tag volume for repair
+        df2.loc[f_change & f_vol_zero_or_nan, "Volume"] = tag
+
+        df2_tagged = df2[data_cols].to_numpy() == tag
+        n_before = df2_tagged.sum()
+        dts_tagged = df2.index[df2_tagged.any(axis=1)]
+        df2 = self._reconstruct_intervals_batch(df2, interval, prepost, tag)
+        df2_tagged = df2[data_cols].to_numpy() == tag
+        n_after = df2_tagged.sum()
+        dts_not_repaired = df2.index[df2_tagged.any(axis=1)]
+        n_fixed = n_before - n_after
+        if n_fixed > 0:
+            msg = f"{self.ticker}: fixed {n_fixed}/{n_before} value=0 errors in {interval} price data"
+            if n_fixed < 4:
+                dts_repaired = sorted(list(set(dts_tagged).difference(dts_not_repaired)))
+                msg += f": {dts_repaired}"
+            logger.info('price-repair-missing: ' + msg)
+
+        if df2_reserve is not None:
+            if "Repaired?" not in df2_reserve.columns:
+                df2_reserve["Repaired?"] = False
+            df2 = pd.concat([df2, df2_reserve]).sort_index()
+
+        # Restore original values where repair failed (i.e. remove tag values)
+        f = df2[data_cols].to_numpy() == tag
+        for j in range(len(data_cols)):
+            fj = f[:, j]
+            if fj.any():
+                c = data_cols[j]
+                df2.loc[fj, c] = df.loc[fj, c]
+
+        return df2
+
+    @utils.log_indent_decorator
+    def _fix_missing_div_adjust(self, df, interval, tz_exchange):
+        # Sometimes, if a dividend occurred today, then Yahoo has not adjusted historic data.
+        # Easy to detect and correct BUT ONLY IF the data 'df' includes today's dividend.
+        # E.g. if fetching historic prices before todays dividend, then cannot fix.
+
+        if df.empty:
+            return df
+
+        logger = utils.get_yf_logger()
+
+        if df is None or df.empty:
+            return df
+        interday = interval in ['1d', '1wk', '1mo', '3mo']
+        if not interday:
+            return df
+
+        df = df.sort_index()
+
+        f_div = (df["Dividends"] != 0.0).to_numpy()
+        if not f_div.any():
+            logger.debug('div-adjust-repair: No dividends to check')
+            return df
+
+        df2 = df.copy()
+        if df2.index.tz is None:
+            df2.index = df2.index.tz_localize(tz_exchange)
+        elif df2.index.tz != tz_exchange:
+            df2.index = df2.index.tz_convert(tz_exchange)
+
+        div_indices = np.where(f_div)[0]
+        last_div_idx = div_indices[-1]
+        if last_div_idx == 0:
+            # Not enough data to recalculate the div-adjustment,
+            # because need close day before
+            logger.debug('div-adjust-repair: Insufficient data to recalculate div-adjustment')
+            return df
+
+        # To determine if Yahoo messed up, analyse price data between today's dividend and
+        # the previous dividend
+        if len(div_indices) == 1:
+            # No other divs in data
+            prev_idx = 0
+            prev_dt = None
+        else:
+            prev_idx = div_indices[-2]
+            prev_dt = df2.index[prev_idx]
+        f_no_adj = (df2['Close'] == df2['Adj Close']).to_numpy()[prev_idx:last_div_idx]
+        threshold_pct = 0.5
+        Yahoo_failed = (np.sum(f_no_adj) / len(f_no_adj)) > threshold_pct
+
+        # Fix Yahoo
+        if Yahoo_failed:
+            last_div_dt = df2.index[last_div_idx]
+            last_div_row = df2.loc[last_div_dt]
+            close_day_before = df2['Close'].iloc[last_div_idx - 1]
+            adj = 1.0 - df2['Dividends'].iloc[last_div_idx] / close_day_before
+            div = last_div_row['Dividends']
+            msg = f'Correcting missing div-adjustment preceding div = {div} @ {last_div_dt.date()} (prev_dt={prev_dt})'
+            logger.debug('div-adjust-repair: ' + msg)
+
+            if interval == '1d':
+                # exclusive
+                df2.loc[:last_div_dt - _datetime.timedelta(seconds=1), 'Adj Close'] *= adj
+            else:
+                # inclusive
+                df2.loc[:last_div_dt, 'Adj Close'] *= adj
+
+        return df2
+
+    @utils.log_indent_decorator
+    def _fix_bad_stock_split(self, df, interval, tz_exchange):
+        # Repair idea is to look for BIG daily price changes that closely match the
+        # most recent stock split ratio. This indicates Yahoo failed to apply a new
+        # stock split to old price data.
+        #
+        # There is a slight complication, because Yahoo does another stupid thing.
+        # Sometimes the old data is adjusted twice. So cannot simply assume
+        # which direction to reverse adjustment - have to analyse prices and detect.
+        # Not difficult.
+
+        if df.empty:
+            return df
+
+        logger = utils.get_yf_logger()
+
+        interday = interval in ['1d', '1wk', '1mo', '3mo']
+        if not interday:
+            return df
+
+        # Find the most recent stock split
+        df = df.sort_index(ascending=False)
+        split_f = df['Stock Splits'].to_numpy() != 0
+        if not split_f.any():
+            logger.debug('price-repair-split: No splits in data')
+            return df
+        most_recent_split_day = df.index[split_f].max()
+        split = df.loc[most_recent_split_day, 'Stock Splits']
+        if most_recent_split_day == df.index[0]:
+            logger.info(
+                "price-repair-split: Need 1+ day of price data after split to determine true price. Won't repair")
+            return df
+
+        logger.debug(f'price-repair-split: Most recent split = {split:.4f} @ {most_recent_split_day.date()}')
+
+        return self._fix_prices_sudden_change(df, interval, tz_exchange, split, correct_volume=True)
+
+    @utils.log_indent_decorator
+    def _fix_prices_sudden_change(self, df, interval, tz_exchange, change, correct_volume=False):
+        if df.empty:
+            return df
+
+        logger = utils.get_yf_logger()
+
+        split = change
+        split_rcp = 1.0 / split
+        interday = interval in ['1d', '1wk', '1mo', '3mo']
+
+        if change in [100.0, 0.01]:
+            fix_type = '100x error'
+            start_min = None
+        else:
+            fix_type = 'bad split'
+            # start_min = 1 year before oldest split
+            f = df['Stock Splits'].to_numpy() != 0.0
+            start_min = (df.index[f].min() - _dateutil.relativedelta.relativedelta(years=1)).date()
+            logger.debug(f'price-repair-split: start_min={start_min}')
+
+        OHLC = ['Open', 'High', 'Low', 'Close']
+
+        # Do not attempt repair of the split is small,
+        # could be mistaken for normal price variance
+        if 0.8 < split < 1.25:
+            logger.info("price-repair-split: Split ratio too close to 1. Won't repair")
+            return df
+
+        df2 = df.copy().sort_index(ascending=False)
+        if df2.index.tz is None:
+            df2.index = df2.index.tz_localize(tz_exchange)
+        elif df2.index.tz != tz_exchange:
+            df2.index = df2.index.tz_convert(tz_exchange)
+        n = df2.shape[0]
+
+        # If stock is currently suspended and not in USA, then usually Yahoo introduces
+        # 100x errors into suspended intervals. Clue is no price change and 0 volume.
+        # Better to use last active trading interval as baseline.
+        f_no_activity = (df2['Low'] == df2['High']) & (df2['Volume']==0)
+        f_no_activity = f_no_activity | df2[OHLC].isna().all(axis=1)
+        appears_suspended = f_no_activity.any() and np.where(f_no_activity)[0][0]==0
+        f_active = ~f_no_activity
+        idx_latest_active = np.where(f_active & np.roll(f_active, 1))[0]
+        if len(idx_latest_active) == 0:
+            idx_latest_active = None
+        else:
+            idx_latest_active = int(idx_latest_active[0])
+        log_msg = f'price-repair-split: appears_suspended={appears_suspended}, idx_latest_active={idx_latest_active}'
+        if idx_latest_active is not None:
+            log_msg += f' ({df2.index[idx_latest_active].date()})'
+        logger.debug(log_msg)
+
+        if logger.isEnabledFor(logging.DEBUG):
+            df_debug = df2.copy()
+            df_debug = df_debug.drop(['Adj Close', 'Volume', 'Dividends', 'Repaired?'], axis=1, errors='ignore')
+            debug_cols = ['Low', 'High']
+            df_debug = df_debug.drop([c for c in OHLC if c not in debug_cols], axis=1, errors='ignore')
+        else:
+            debug_cols = []
+
+        # Calculate daily price % change. To reduce effect of price volatility,
+        # calculate change for each OHLC column.
+        if interday and interval != '1d' and split not in [100.0, 100, 0.001]:
+            # Avoid using 'Low' and 'High'. For multiday intervals, these can be
+            # very volatile so reduce ability to detect genuine stock split errors
+            _1d_change_x = np.full((n, 2), 1.0)
+            price_data = df2[['Open','Close']].to_numpy()
+            f_zero = price_data == 0.0
+        else:
+            _1d_change_x = np.full((n, 4), 1.0)
+            price_data = df2[OHLC].to_numpy()
+            f_zero = price_data == 0.0
+        if f_zero.any():
+            price_data[f_zero] = 1.0
+
+        # Update: if a VERY large dividend is paid out, then can be mistaken for a 1:2 stock split.
+        # Fix = use adjusted prices
+        adj = df2['Adj Close'].to_numpy() / df2['Close'].to_numpy()
+        df_dtype = price_data.dtype
+        if df_dtype == np.int64:
+            price_data = price_data.astype('float')
+        for j in range(price_data.shape[1]):
+            price_data[:,j] *= adj
+        if df_dtype == np.int64:
+            price_data = price_data.astype('int')
+
+        _1d_change_x[1:] = price_data[1:, ] / price_data[:-1, ]
+        f_zero_num_denom = f_zero | np.roll(f_zero, 1, axis=0)
+        if f_zero_num_denom.any():
+            _1d_change_x[f_zero_num_denom] = 1.0
+        if interday and interval != '1d':
+            # average change
+            _1d_change_minx = np.average(_1d_change_x, axis=1)
+        else:
+            # change nearest to 1.0
+            diff = np.abs(_1d_change_x - 1.0)
+            j_indices = np.argmin(diff, axis=1)
+            _1d_change_minx = _1d_change_x[np.arange(n), j_indices]
+        f_na = np.isnan(_1d_change_minx)
+        if f_na.any():
+            # Possible if data was too old for reconstruction.
+            _1d_change_minx[f_na] = 1.0
+        if logger.isEnabledFor(logging.DEBUG):
+            df_debug['1D change X'] = _1d_change_minx
+            df_debug['1D change X'] = df_debug['1D change X'].round(2).astype('str')
+
+        # If all 1D changes are closer to 1.0 than split, exit
+        split_max = max(split, split_rcp)
+        if np.max(_1d_change_minx) < (split_max - 1) * 0.5 + 1 and np.min(_1d_change_minx) > 1.0 / ((split_max - 1) * 0.5 + 1):
+            logger.info(f"price-repair-split: No {fix_type}s detected")
+            return df
+
+        # Calculate the true price variance, i.e. remove effect of bad split-adjustments.
+        # Key = ignore 1D changes outside of interquartile range
+        q1, q3 = np.percentile(_1d_change_minx, [25, 75])
+        iqr = q3 - q1
+        lower_bound = q1 - 1.5 * iqr
+        upper_bound = q3 + 1.5 * iqr
+        f = (_1d_change_minx >= lower_bound) & (_1d_change_minx <= upper_bound)
+        avg = np.mean(_1d_change_minx[f])
+        sd = np.std(_1d_change_minx[f])
+        # Now can calculate SD as % of mean
+        sd_pct = sd / avg
+        logger.debug(f"price-repair-split: Estimation of true 1D change stats: mean = {avg:.2f}, StdDev = {sd:.4f} ({sd_pct*100.0:.1f}% of mean)")
+
+        # Only proceed if split adjustment far exceeds normal 1D changes
+        largest_change_pct = 5 * sd_pct
+        if interday and interval != '1d':
+            largest_change_pct *= 3
+            if interval in ['1mo', '3mo']:
+                largest_change_pct *= 2
+        if max(split, split_rcp) < 1.0 + largest_change_pct:
+            logger.info("price-repair-split: Split ratio too close to normal price volatility. Won't repair")
+            logger.debug(f"sd_pct = {sd_pct:.4f}  largest_change_pct = {largest_change_pct:.4f}")
+            if logger.isEnabledFor(logging.DEBUG):
+                logger.debug(f"sd_pct = {sd_pct:.4f}  largest_change_pct = {largest_change_pct:.4f}")
+            return df
+
+        # Now can detect bad split adjustments
+        # Set threshold to halfway between split ratio and largest expected normal price change
+        r = _1d_change_minx / split_rcp
+        split_max = max(split, split_rcp)
+        logger.debug(f"price-repair-split: split_max={split_max:.3f} largest_change_pct={largest_change_pct:.4f}")
+        threshold = (split_max + 1.0 + largest_change_pct) * 0.5
+        logger.debug(f"price-repair-split: threshold={threshold:.3f}")
+
+        if 'Repaired?' not in df2.columns:
+            df2['Repaired?'] = False
+
+        if interday and interval != '1d':
+            # Yahoo creates multi-day intervals using potentiall corrupt data, e.g.
+            # the Close could be 100x Open. This means have to correct each OHLC column
+            # individually
+            correct_columns_individually = True
+        else:
+            correct_columns_individually = False
+
+        if correct_columns_individually:
+            _1d_change_x = np.full((n, 4), 1.0)
+            price_data = df2[OHLC].replace(0.0, 1.0).to_numpy()
+            _1d_change_x[1:] = price_data[1:, ] / price_data[:-1, ]
+        else:
+            _1d_change_x = _1d_change_minx
+
+        r = _1d_change_x / split_rcp
+        f_down = _1d_change_x < 1.0 / threshold
+        f_up = _1d_change_x > threshold
+        f = f_down | f_up
+        if logger.isEnabledFor(logging.DEBUG):
+            if not correct_columns_individually:
+                df_debug['r'] = r
+                df_debug['f_down'] = f_down
+                df_debug['f_up'] = f_up
+                df_debug['r'] = df_debug['r'].round(2).astype('str')
+            else:
+                for j in range(len(OHLC)):
+                    c = OHLC[j]
+                    if c in debug_cols:
+                        df_debug[c + '_r'] = r[:, j]
+                        df_debug[c + '_f_down'] = f_down[:, j]
+                        df_debug[c + '_f_up'] = f_up[:, j]
+                        df_debug[c + '_r'] = df_debug[c + '_r'].round(2).astype('str')
+
+
+        if not f.any():
+            logger.info(f'price-repair-split: No {fix_type}s detected')
+            return df
+
+        # Update: if any 100x changes are soon after a stock split, so could be confused with split error, then abort
+        threshold_days = 30
+        f_splits = df2['Stock Splits'].to_numpy() != 0.0
+        if change in [100.0, 0.01] and f_splits.any():
+            indices_A = np.where(f_splits)[0]
+            indices_B = np.where(f)[0]
+            if not len(indices_A) or not len(indices_B):
+                return None
+            gaps = indices_B[:, None] - indices_A
+            # Because data is sorted in DEscending order, need to flip gaps
+            gaps *= -1
+            f_pos = gaps > 0
+            if f_pos.any():
+                gap_min = gaps[f_pos].min()
+                gap_td = utils._interval_to_timedelta(interval) * gap_min
+                if isinstance(gap_td, _dateutil.relativedelta.relativedelta):
+                    threshold = _dateutil.relativedelta.relativedelta(days=threshold_days)
+                else:
+                    threshold = _datetime.timedelta(days=threshold_days)
+                if gap_td < threshold:
+                    logger.info('price-repair-split: 100x changes are too soon after stock split events, aborting')
+                    return df
+
+        # if logger.isEnabledFor(logging.DEBUG):
+        #     df_debug['i'] = list(range(0, df_debug.shape[0]))
+        #     df_debug['i_rev'] = df_debug.shape[0]-1 - df_debug['i']
+        #     with pd.option_context('display.max_rows', None, 'display.max_columns', 10, 'display.width', 1000):  # more options can be specified also
+        #         logger.debug(f"price-repair-split: my workings:" + '\n' + str(df_debug))
+
+        def map_signals_to_ranges(f, f_up, f_down):
+            # Ensure 0th element is False, because True is nonsense
+            if f[0]:
+                f = np.copy(f)
+                f[0] = False
+                f_up = np.copy(f_up)
+                f_up[0] = False
+                f_down = np.copy(f_down)
+                f_down[0] = False
+
+            if not f.any():
+                return []
+
+            true_indices = np.where(f)[0]
+            ranges = []
+
+            for i in range(len(true_indices) - 1):
+                if i % 2 == 0:
+                    if split > 1.0:
+                        adj = 'split' if f_down[true_indices[i]] else '1.0/split'
+                    else:
+                        adj = '1.0/split' if f_down[true_indices[i]] else 'split'
+                    ranges.append((true_indices[i], true_indices[i + 1], adj))
+
+            if len(true_indices) % 2 != 0:
+                if split > 1.0:
+                    adj = 'split' if f_down[true_indices[-1]] else '1.0/split'
+                else:
+                    adj = '1.0/split' if f_down[true_indices[-1]] else 'split'
+                ranges.append((true_indices[-1], len(f), adj))
+
+            return ranges
+
+        if idx_latest_active is not None:
+            idx_rev_latest_active = df.shape[0] - 1 - idx_latest_active
+            logger.debug(f'price-repair-split: idx_latest_active={idx_latest_active}, idx_rev_latest_active={idx_rev_latest_active}')
+        if correct_columns_individually:
+            f_corrected = np.full(n, False)
+            if correct_volume:
+                # If Open or Close is repaired but not both,
+                # then this means the interval has a mix of correct
+                # and errors. A problem for correcting Volume,
+                # so use a heuristic:
+                # - if both Open & Close were Nx bad => Volume is Nx bad
+                # - if only one of Open & Close are Nx bad => Volume is 0.5*Nx bad
+                f_open_fixed = np.full(n, False)
+                f_close_fixed = np.full(n, False)
+
+            OHLC_correct_ranges = [None, None, None, None]
+            for j in range(len(OHLC)):
+                c = OHLC[j]
+                idx_first_f = np.where(f)[0][0]
+                if appears_suspended and (idx_latest_active is not None and idx_latest_active >= idx_first_f):
+                    # Suspended midway during data date range.
+                    # 1: process data before suspension in index-ascending (date-descending) order.
+                    # 2: process data after suspension in index-descending order. Requires signals to be reversed,
+                    #    then returned ranges to also be reversed, because this logic was originally written for
+                    #    index-ascending (date-descending) order.
+                    fj = f[:, j]
+                    f_upj = f_up[:, j]
+                    f_downj = f_down[:, j]
+                    ranges_before = map_signals_to_ranges(fj[idx_latest_active:], f_upj[idx_latest_active:], f_downj[idx_latest_active:])
+                    if len(ranges_before) > 0:
+                        # Shift each range back to global indexing
+                        for i in range(len(ranges_before)):
+                            r = ranges_before[i]
+                            ranges_before[i] = (r[0] + idx_latest_active, r[1] + idx_latest_active, r[2])
+                    f_rev_downj = np.flip(np.roll(f_upj, -1))  # correct
+                    f_rev_upj = np.flip(np.roll(f_downj, -1))  # correct
+                    f_revj = f_rev_upj | f_rev_downj
+                    ranges_after = map_signals_to_ranges(f_revj[idx_rev_latest_active:], f_rev_upj[idx_rev_latest_active:], f_rev_downj[idx_rev_latest_active:])
+                    if len(ranges_after) > 0:
+                        # Shift each range back to global indexing:
+                        for i in range(len(ranges_after)):
+                            r = ranges_after[i]
+                            ranges_after[i] = (r[0] + idx_rev_latest_active, r[1] + idx_rev_latest_active, r[2])
+                        # Flip range to normal ordering
+                        for i in range(len(ranges_after)):
+                            r = ranges_after[i]
+                            ranges_after[i] = (n-r[1], n-r[0], r[2])
+                    ranges = ranges_before
+                    ranges.extend(ranges_after)
+                else:
+                    ranges = map_signals_to_ranges(f[:, j], f_up[:, j], f_down[:, j])
+                logger.debug(f"column '{c}' ranges: {ranges}")
+                if start_min is not None:
+                    # Prune ranges that are older than start_min
+                    for i in range(len(ranges)-1, -1, -1):
+                        r = ranges[i]
+                        if df2.index[r[0]].date() < start_min:
+                            logger.debug(f'price-repair-split: Pruning {c} range {df2.index[r[0]]}->{df2.index[r[1]-1]} because too old.')
+                            del ranges[i]
+
+                if len(ranges) > 0:
+                    OHLC_correct_ranges[j] = ranges
+
+            count = sum([1 if x is not None else 0 for x in OHLC_correct_ranges])
+            if count == 0:
+                pass
+            elif count == 1:
+                # If only 1 column then assume false positive
+                idxs = [i if OHLC_correct_ranges[i] else -1 for i in range(len(OHLC))]
+                idx = np.where(np.array(idxs) != -1)[0][0]
+                col = OHLC[idx]
+                logger.debug(f'price-repair-split: Potential {fix_type} detected only in column {col}, so treating as false positive (ignore)')
+            else:
+                # Only correct if at least 2 columns require correction.
+                for j in range(len(OHLC)):
+                    c = OHLC[j]
+                    ranges = OHLC_correct_ranges[j]
+                    if ranges is None:
+                        ranges = []
+                    for r in ranges:
+                        if r[2] == 'split':
+                            m = split
+                            m_rcp = split_rcp
+                        else:
+                            m = split_rcp
+                            m_rcp = split
+                        if interday:
+                            logger.info(f"price-repair-split: Corrected {fix_type} on col={c} range=[{df2.index[r[1]-1].date()}:{df2.index[r[0]].date()}] m={m:.4f}")
+                        else:
+                            logger.info(f"price-repair-split: Corrected {fix_type} on col={c} range=[{df2.index[r[1]-1]}:{df2.index[r[0]]}] m={m:.4f}")
+                        df2.iloc[r[0]:r[1], df2.columns.get_loc(c)] *= m
+                        if c == 'Close':
+                            df2.iloc[r[0]:r[1], df2.columns.get_loc('Adj Close')] *= m
+                        if correct_volume:
+                            if c == 'Open':
+                                f_open_fixed[r[0]:r[1]] = True
+                            elif c == 'Close':
+                                f_close_fixed[r[0]:r[1]] = True
+                        f_corrected[r[0]:r[1]] = True
+
+            if correct_volume:
+                f_open_and_closed_fixed = f_open_fixed & f_close_fixed
+                f_open_xor_closed_fixed = np.logical_xor(f_open_fixed, f_close_fixed)
+                if f_open_and_closed_fixed.any():
+                    df2.loc[f_open_and_closed_fixed, "Volume"] *= m_rcp
+                if f_open_xor_closed_fixed.any():
+                    df2.loc[f_open_xor_closed_fixed, "Volume"] *= 0.5 * m_rcp
+
+            df2.loc[f_corrected, 'Repaired?'] = True
+
+        else:
+            idx_first_f = np.where(f)[0][0]
+            if appears_suspended and (idx_latest_active is not None and idx_latest_active >= idx_first_f):
+                # Suspended midway during data date range.
+                # 1: process data before suspension in index-ascending (date-descending) order.
+                # 2: process data after suspension in index-descending order. Requires signals to be reversed,
+                #    then returned ranges to also be reversed, because this logic was originally written for
+                #    index-ascending (date-descending) order.
+                ranges_before = map_signals_to_ranges(f[idx_latest_active:], f_up[idx_latest_active:], f_down[idx_latest_active:])
+                if len(ranges_before) > 0:
+                    # Shift each range back to global indexing
+                    for i in range(len(ranges_before)):
+                        r = ranges_before[i]
+                        ranges_before[i] = (r[0] + idx_latest_active, r[1] + idx_latest_active, r[2])
+                f_rev_down = np.flip(np.roll(f_up, -1))
+                f_rev_up = np.flip(np.roll(f_down, -1))
+                f_rev = f_rev_up | f_rev_down
+                ranges_after = map_signals_to_ranges(f_rev[idx_rev_latest_active:], f_rev_up[idx_rev_latest_active:], f_rev_down[idx_rev_latest_active:])
+                if len(ranges_after) > 0:
+                    # Shift each range back to global indexing:
+                    for i in range(len(ranges_after)):
+                        r = ranges_after[i]
+                        ranges_after[i] = (r[0] + idx_rev_latest_active, r[1] + idx_rev_latest_active, r[2])
+                    # Flip range to normal ordering
+                    for i in range(len(ranges_after)):
+                        r = ranges_after[i]
+                        ranges_after[i] = (n-r[1], n-r[0], r[2])
+                ranges = ranges_before
+                ranges.extend(ranges_after)
+            else:
+                ranges = map_signals_to_ranges(f, f_up, f_down)
+            if start_min is not None:
+                # Prune ranges that are older than start_min
+                for i in range(len(ranges)-1, -1, -1):
+                    r = ranges[i]
+                    if df2.index[r[0]].date() < start_min:
+                        logger.debug(f'price-repair-split: Pruning range {df2.index[r[0]]}->{df2.index[r[1]-1]} because too old.')
+                        del ranges[i]
+            for r in ranges:
+                if r[2] == 'split':
+                    m = split
+                    m_rcp = split_rcp
+                else:
+                    m = split_rcp
+                    m_rcp = split
+                logger.debug(f"price-repair-split: range={r} m={m}")
+                for c in ['Open', 'High', 'Low', 'Close', 'Adj Close']:
+                    df2.iloc[r[0]:r[1], df2.columns.get_loc(c)] *= m
+                if correct_volume:
+                    df2.iloc[r[0]:r[1], df2.columns.get_loc("Volume")] *= m_rcp
+                df2.iloc[r[0]:r[1], df2.columns.get_loc('Repaired?')] = True
+                if r[0] == r[1] - 1:
+                    if interday:
+                        msg = f"price-repair-split: Corrected {fix_type} on interval {df2.index[r[0]].date()}"
+                    else:
+                        msg = f"price-repair-split: Corrected {fix_type} on interval {df2.index[r[0]]}"
+                else:
+                    # Note: df2 sorted with index descending
+                    start = df2.index[r[1] - 1]
+                    end = df2.index[r[0]]
+                    if interday:
+                        msg = f"price-repair-split: Corrected {fix_type} across intervals {start.date()} -> {end.date()} (inclusive)"
+                    else:
+                        msg = f"price-repair-split: Corrected {fix_type} across intervals {start} -> {end} (inclusive)"
+                logger.info(msg)
+
+        if correct_volume:
+            f_na = df2['Volume'].isna()
+            if f_na.any():
+                df2.loc[~f_na,'Volume'] = df2['Volume'][~f_na].round(0).astype('int')
+            else:
+                df2['Volume'] = df2['Volume'].round(0).astype('int')
+
+        return df2.sort_index()
diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py
index 1d018359b..7df7e102d 100644
--- a/yfinance/scrapers/quote.py
+++ b/yfinance/scrapers/quote.py
@@ -1,6 +1,5 @@
 import datetime
 import json
-import logging
 import warnings
 from collections.abc import MutableMapping
 
@@ -182,10 +181,7 @@ def toJSON(self, indent=4):
 
     def _get_1y_prices(self, fullDaysOnly=False):
         if self._prices_1y is None:
-            # Temporarily disable error printing
-            logging.disable(logging.CRITICAL)
             self._prices_1y = self._tkr.history(period="380d", auto_adjust=False, keepna=True, proxy=self.proxy)
-            logging.disable(logging.NOTSET)
             self._md = self._tkr.get_history_metadata(proxy=self.proxy)
             try:
                 ctp = self._md["currentTradingPeriod"]
@@ -211,18 +207,12 @@ def _get_1y_prices(self, fullDaysOnly=False):
 
     def _get_1wk_1h_prepost_prices(self):
         if self._prices_1wk_1h_prepost is None:
-            # Temporarily disable error printing
-            logging.disable(logging.CRITICAL)
             self._prices_1wk_1h_prepost = self._tkr.history(period="1wk", interval="1h", auto_adjust=False, prepost=True, proxy=self.proxy)
-            logging.disable(logging.NOTSET)
         return self._prices_1wk_1h_prepost
 
     def _get_1wk_1h_reg_prices(self):
         if self._prices_1wk_1h_reg is None:
-            # Temporarily disable error printing
-            logging.disable(logging.CRITICAL)
             self._prices_1wk_1h_reg = self._tkr.history(period="1wk", interval="1h", auto_adjust=False, prepost=False, proxy=self.proxy)
-            logging.disable(logging.NOTSET)
         return self._prices_1wk_1h_reg
 
     def _get_exchange_metadata(self):
@@ -261,8 +251,6 @@ def currency(self):
         if self._currency is not None:
             return self._currency
 
-        if self._tkr._history_metadata is None:
-            self._get_1y_prices()
         md = self._tkr.get_history_metadata(proxy=self.proxy)
         self._currency = md["currency"]
         return self._currency
@@ -272,8 +260,6 @@ def quote_type(self):
         if self._quote_type is not None:
             return self._quote_type
 
-        if self._tkr._history_metadata is None:
-            self._get_1y_prices()
         md = self._tkr.get_history_metadata(proxy=self.proxy)
         self._quote_type = md["instrumentType"]
         return self._quote_type
diff --git a/yfinance/ticker.py b/yfinance/ticker.py
index 75ba14029..075f457d5 100644
--- a/yfinance/ticker.py
+++ b/yfinance/ticker.py
@@ -27,6 +27,7 @@
 import pandas as _pd
 
 from .base import TickerBase
+from .const import _BASE_URL_
 
 
 class Ticker(TickerBase):
@@ -40,9 +41,9 @@ def __repr__(self):
 
     def _download_options(self, date=None):
         if date is None:
-            url = f"{self._base_url}/v7/finance/options/{self.ticker}"
+            url = f"{_BASE_URL_}/v7/finance/options/{self.ticker}"
         else:
-            url = f"{self._base_url}/v7/finance/options/{self.ticker}?date={date}"
+            url = f"{_BASE_URL_}/v7/finance/options/{self.ticker}?date={date}"
 
         r = self._data.get(url=url, proxy=self.proxy).json()
         if len(r.get('optionChain', {}).get('result', [])) > 0:
diff --git a/yfinance/utils.py b/yfinance/utils.py
index 53eac44a9..ee3997c09 100644
--- a/yfinance/utils.py
+++ b/yfinance/utils.py
@@ -689,7 +689,7 @@ def safe_merge_dfs(df_main, df_sub, interval):
                 df_main['Dividends'] = 0.0
                 return df_main
         else:
-            empty_row_data = {**{c:[_np.nan] for c in const.price_colnames}, 'Volume':[0]}
+            empty_row_data = {**{c:[_np.nan] for c in const._PRICE_COLNAMES_}, 'Volume':[0]}
             if interval == '1d':
                 # For 1d, add all out-of-range event dates
                 for i in _np.where(f_outOfRange)[0]:
@@ -772,7 +772,7 @@ def fix_Yahoo_dst_issue(df, interval):
         f_pre_midnight = (df.index.minute == 0) & (df.index.hour.isin([22, 23]))
         dst_error_hours = _np.array([0] * df.shape[0])
         dst_error_hours[f_pre_midnight] = 24 - df.index[f_pre_midnight].hour
-        df.index += _pd.TimedeltaIndex(dst_error_hours, 'h')
+        df.index += _pd.to_timedelta(dst_error_hours, 'h')
     return df
 
 


Test patch:
diff --git a/tests/prices.py b/tests/prices.py
index c0fa4df0a..61f633249 100644
--- a/tests/prices.py
+++ b/tests/prices.py
@@ -277,7 +277,7 @@ def test_dailyWithEvents_bugs(self):
         # Reproduce issue #1634 - 1d dividend out-of-range, should be prepended to prices
         div_dt = _pd.Timestamp(2022, 7, 21).tz_localize("America/New_York")
         df_dividends = _pd.DataFrame(data={"Dividends":[1.0]}, index=[div_dt])
-        df_prices = _pd.DataFrame(data={c:[1.0] for c in yf.const.price_colnames}|{'Volume':0}, index=[div_dt+_dt.timedelta(days=1)])
+        df_prices = _pd.DataFrame(data={c:[1.0] for c in yf.const._PRICE_COLNAMES_}|{'Volume':0}, index=[div_dt+_dt.timedelta(days=1)])
         df_merged = yf.utils.safe_merge_dfs(df_prices, df_dividends, '1d')
         self.assertEqual(df_merged.shape[0], 2)
         self.assertTrue(df_merged[df_prices.columns].iloc[1:].equals(df_prices))
@@ -470,6 +470,18 @@ def tearDownClass(cls):
         if cls.session is not None:
             cls.session.close()
 
+    def test_types(self):
+        tkr = 'INTC'
+        dat = yf.Ticker(tkr, session=self.session)
+
+        data = dat.history(period="3mo", interval="1d", prepost=True, repair=True)
+        self.assertIsInstance(data, _pd.DataFrame, "data has wrong type")
+        self.assertFalse(data.empty, "data is empty")
+
+        reconstructed = dat._lazy_load_price_history()._reconstruct_intervals_batch(data, "1wk", True)
+        self.assertIsInstance(reconstructed, _pd.DataFrame, "data has wrong type")
+        self.assertFalse(data.empty, "data is empty")
+
     def test_reconstruct_2m(self):
         # 2m repair requires 1m data.
         # Yahoo restricts 1m fetches to 7 days max within last 30 days.
@@ -494,6 +506,7 @@ def test_repair_100x_random_weekly(self):
         tkr = "PNL.L"
         dat = yf.Ticker(tkr, session=self.session)
         tz_exchange = dat.fast_info["timezone"]
+        hist = dat._lazy_load_price_history()
 
         data_cols = ["Low", "High", "Open", "Close", "Adj Close"]
         df = _pd.DataFrame(data={"Open":      [470.5, 473.5, 474.5, 470],
@@ -517,7 +530,7 @@ def test_repair_100x_random_weekly(self):
 
         # Run test
 
-        df_repaired = dat._fix_unit_random_mixups(df_bad, "1wk", tz_exchange, prepost=False)
+        df_repaired = hist._fix_unit_random_mixups(df_bad, "1wk", tz_exchange, prepost=False)
 
         # First test - no errors left
         for c in data_cols:
@@ -548,6 +561,7 @@ def test_repair_100x_random_weekly_preSplit(self):
         tkr = "PNL.L"
         dat = yf.Ticker(tkr, session=self.session)
         tz_exchange = dat.fast_info["timezone"]
+        hist = dat._lazy_load_price_history()
 
         data_cols = ["Low", "High", "Open", "Close", "Adj Close"]
         df = _pd.DataFrame(data={"Open":      [400,    398,    392.5,  417],
@@ -574,7 +588,7 @@ def test_repair_100x_random_weekly_preSplit(self):
         df.index = df.index.tz_localize(tz_exchange)
         df_bad.index = df_bad.index.tz_localize(tz_exchange)
 
-        df_repaired = dat._fix_unit_random_mixups(df_bad, "1wk", tz_exchange, prepost=False)
+        df_repaired = hist._fix_unit_random_mixups(df_bad, "1wk", tz_exchange, prepost=False)
 
         # First test - no errors left
         for c in data_cols:
@@ -606,6 +620,7 @@ def test_repair_100x_random_daily(self):
         tkr = "PNL.L"
         dat = yf.Ticker(tkr, session=self.session)
         tz_exchange = dat.fast_info["timezone"]
+        hist = dat._lazy_load_price_history()
 
         data_cols = ["Low", "High", "Open", "Close", "Adj Close"]
         df = _pd.DataFrame(data={"Open":      [478,    476,   476,   472],
@@ -627,7 +642,7 @@ def test_repair_100x_random_daily(self):
         df.index = df.index.tz_localize(tz_exchange)
         df_bad.index = df_bad.index.tz_localize(tz_exchange)
 
-        df_repaired = dat._fix_unit_random_mixups(df_bad, "1d", tz_exchange, prepost=False)
+        df_repaired = hist._fix_unit_random_mixups(df_bad, "1d", tz_exchange, prepost=False)
 
         # First test - no errors left
         for c in data_cols:
@@ -656,6 +671,7 @@ def test_repair_100x_block_daily(self):
             for interval in ['1d', '1wk']:
                 dat = yf.Ticker(tkr, session=self.session)
                 tz_exchange = dat.fast_info["timezone"]
+                hist = dat._lazy_load_price_history()
 
                 data_cols = ["Low", "High", "Open", "Close", "Adj Close"]
                 _dp = os.path.dirname(__file__)
@@ -672,7 +688,7 @@ def test_repair_100x_block_daily(self):
                 df.index = _pd.to_datetime(df.index, utc=True).tz_convert(tz_exchange)
                 df = df.sort_index()
 
-                df_repaired = dat._fix_unit_switch(df_bad, interval, tz_exchange)
+                df_repaired = hist._fix_unit_switch(df_bad, interval, tz_exchange)
                 df_repaired = df_repaired.sort_index()
 
                 # First test - no errors left
@@ -704,6 +720,7 @@ def test_repair_100x_block_daily(self):
     def test_repair_zeroes_daily(self):
         tkr = "BBIL.L"
         dat = yf.Ticker(tkr, session=self.session)
+        hist = dat._lazy_load_price_history()
         tz_exchange = dat.fast_info["timezone"]
 
         df_bad = _pd.DataFrame(data={"Open":      [0,      102.04, 102.04],
@@ -719,7 +736,7 @@ def test_repair_zeroes_daily(self):
         df_bad.index.name = "Date"
         df_bad.index = df_bad.index.tz_localize(tz_exchange)
 
-        repaired_df = dat._fix_zeroes(df_bad, "1d", tz_exchange, prepost=False)
+        repaired_df = hist._fix_zeroes(df_bad, "1d", tz_exchange, prepost=False)
 
         correct_df = df_bad.copy()
         correct_df.loc["2022-11-01", "Open"] = 102.080002
@@ -753,6 +770,7 @@ def test_repair_zeroes_daily_adjClose(self):
         dat = yf.Ticker(tkr, session=self.session)
         tz_exchange = dat.fast_info["timezone"]
         df.index = df.index.tz_localize(tz_exchange)
+        hist = dat._lazy_load_price_history()
 
         rtol = 5e-3
         for i in [0, 1, 2]:
@@ -761,7 +779,7 @@ def test_repair_zeroes_daily_adjClose(self):
                 df_slice_bad = df_slice.copy()
                 df_slice_bad.loc[df_slice_bad.index[j], "Adj Close"] = 0.0
 
-                df_slice_bad_repaired = dat._fix_zeroes(df_slice_bad, "1d", tz_exchange, prepost=False)
+                df_slice_bad_repaired = hist._fix_zeroes(df_slice_bad, "1d", tz_exchange, prepost=False)
                 for c in ["Close", "Adj Close"]:
                     self.assertTrue(_np.isclose(df_slice_bad_repaired[c], df_slice[c], rtol=rtol).all())
                 self.assertTrue("Repaired?" in df_slice_bad_repaired.columns)
@@ -771,8 +789,9 @@ def test_repair_zeroes_hourly(self):
         tkr = "INTC"
         dat = yf.Ticker(tkr, session=self.session)
         tz_exchange = dat.fast_info["timezone"]
+        hist = dat._lazy_load_price_history()
 
-        correct_df = dat.history(period="1wk", interval="1h", auto_adjust=False, repair=True)
+        correct_df = hist.history(period="1wk", interval="1h", auto_adjust=False, repair=True)
 
         df_bad = correct_df.copy()
         bad_idx = correct_df.index[10]
@@ -783,7 +802,7 @@ def test_repair_zeroes_hourly(self):
         df_bad.loc[bad_idx, "Adj Close"] = _np.nan
         df_bad.loc[bad_idx, "Volume"] = 0
 
-        repaired_df = dat._fix_zeroes(df_bad, "1h", tz_exchange, prepost=False)
+        repaired_df = hist._fix_zeroes(df_bad, "1h", tz_exchange, prepost=False)
 
         for c in ["Open", "Low", "High", "Close"]:
             try:
@@ -812,11 +831,12 @@ def test_repair_bad_stock_split(self):
             for interval in intervals:
                 dat = yf.Ticker(tkr, session=self.session)
                 tz_exchange = dat.fast_info["timezone"]
+                hist = dat._lazy_load_price_history()
 
                 _dp = os.path.dirname(__file__)
                 df_good = dat.history(start='2020-01-01', end=_dt.date.today(), interval=interval, auto_adjust=False)
 
-                repaired_df = dat._fix_bad_stock_split(df_good, interval, tz_exchange)
+                repaired_df = hist._fix_bad_stock_split(df_good, interval, tz_exchange)
 
                 # Expect no change from repair
                 df_good = df_good.sort_index()
@@ -836,6 +856,7 @@ def test_repair_bad_stock_split(self):
         for tkr in bad_tkrs:
             dat = yf.Ticker(tkr, session=self.session)
             tz_exchange = dat.fast_info["timezone"]
+            hist = dat._lazy_load_price_history()
 
             _dp = os.path.dirname(__file__)
             interval = '1d'
@@ -846,7 +867,7 @@ def test_repair_bad_stock_split(self):
             df_bad = _pd.read_csv(fp, index_col="Date")
             df_bad.index = _pd.to_datetime(df_bad.index, utc=True)
 
-            repaired_df = dat._fix_bad_stock_split(df_bad, "1d", tz_exchange)
+            repaired_df = hist._fix_bad_stock_split(df_bad, "1d", tz_exchange)
 
             fp = os.path.join(_dp, "data", tkr.replace('.','-')+'-'+interval+"-bad-stock-split-fixed.csv")
             correct_df = _pd.read_csv(fp, index_col="Date")
@@ -876,11 +897,12 @@ def test_repair_bad_stock_split(self):
             for interval in intervals:
                 dat = yf.Ticker(tkr, session=self.session)
                 tz_exchange = dat.fast_info["timezone"]
+                hist = dat._lazy_load_price_history()
 
                 _dp = os.path.dirname(__file__)
-                df_good = dat.history(start='2020-11-30', end='2021-04-01', interval=interval, auto_adjust=False)
+                df_good = hist.history(start='2020-11-30', end='2021-04-01', interval=interval, auto_adjust=False)
 
-                repaired_df = dat._fix_bad_stock_split(df_good, interval, tz_exchange)
+                repaired_df = hist._fix_bad_stock_split(df_good, interval, tz_exchange)
 
                 # Expect no change from repair
                 df_good = df_good.sort_index()
@@ -900,12 +922,13 @@ def test_repair_missing_div_adjust(self):
 
         dat = yf.Ticker(tkr, session=self.session)
         tz_exchange = dat.fast_info["timezone"]
+        hist = dat._lazy_load_price_history()
 
         _dp = os.path.dirname(__file__)
         df_bad = _pd.read_csv(os.path.join(_dp, "data", tkr.replace('.','-')+"-1d-missing-div-adjust.csv"), index_col="Date")
         df_bad.index = _pd.to_datetime(df_bad.index)
 
-        repaired_df = dat._fix_missing_div_adjust(df_bad, "1d", tz_exchange)
+        repaired_df = hist._fix_missing_div_adjust(df_bad, "1d", tz_exchange)
 
         correct_df = _pd.read_csv(os.path.join(_dp, "data", tkr.replace('.','-')+"-1d-missing-div-adjust-fixed.csv"), index_col="Date")
         correct_df.index = _pd.to_datetime(correct_df.index)
diff --git a/tests/ticker.py b/tests/ticker.py
index ff0d8e6c4..c164be283 100644
--- a/tests/ticker.py
+++ b/tests/ticker.py
@@ -247,15 +247,6 @@ def test_actions(self):
         self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
         self.assertFalse(data.empty, "data is empty")
 
-    def test_reconstruct_intervals_batch(self):
-        data = self.ticker.history(period="3mo", interval="1d", prepost=True, repair=True)
-        self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
-        self.assertFalse(data.empty, "data is empty")
-
-        reconstructed = self.ticker._reconstruct_intervals_batch(data, "1wk", True)
-        self.assertIsInstance(reconstructed, pd.DataFrame, "data has wrong type")
-        self.assertFalse(data.empty, "data is empty")
-
 
 class TestTickerEarnings(unittest.TestCase):
     session = None
diff --git a/tests/utils.py b/tests/utils.py
index f9e54c09d..48bb5edad 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -8,6 +8,8 @@
    python -m unittest tests.utils.TestTicker
 
 """
+from unittest import TestSuite
+
 # import pandas as pd
 # import numpy as np
 
@@ -34,16 +36,16 @@ def test_storeTzNoRaise(self):
         tkr = 'AMZN'
         tz1 = "America/New_York"
         tz2 = "London/Europe"
-        cache = yf.utils.get_tz_cache()
+        cache = yf.cache.get_tz_cache()
         cache.store(tkr, tz1)
         cache.store(tkr, tz2)
 
     def test_setTzCacheLocation(self):
-        self.assertEqual(yf.utils._DBManager.get_location(), self.tempCacheDir.name)
+        self.assertEqual(yf.cache._TzDBManager.get_location(), self.tempCacheDir.name)
 
         tkr = 'AMZN'
         tz1 = "America/New_York"
-        cache = yf.utils.get_tz_cache()
+        cache = yf.cache.get_tz_cache()
         cache.store(tkr, tz1)
 
         self.assertTrue(os.path.exists(os.path.join(self.tempCacheDir.name, "tkr-tz.db")))
@@ -60,10 +62,10 @@ def test_tzCacheRootStore(self):
         tz1 = "America/New_York"
 
         # During attempt to store, will discover cannot write
-        yf.utils.get_tz_cache().store(tkr, tz1)
+        yf.cache.get_tz_cache().store(tkr, tz1)
 
         # Handling the store failure replaces cache with a dummy
-        cache = yf.utils.get_tz_cache()
+        cache = yf.cache.get_tz_cache()
         self.assertTrue(cache.dummy)
         cache.store(tkr, tz1)
 
@@ -71,19 +73,19 @@ def test_tzCacheRootLookup(self):
         # Test that if cache path in read-only filesystem, no exception.
         tkr = 'AMZN'
         # During attempt to lookup, will discover cannot write
-        yf.utils.get_tz_cache().lookup(tkr)
+        yf.cache.get_tz_cache().lookup(tkr)
 
         # Handling the lookup failure replaces cache with a dummy
-        cache = yf.utils.get_tz_cache()
+        cache = yf.cache.get_tz_cache()
         self.assertTrue(cache.dummy)
         cache.lookup(tkr)
 
 
 def suite():
-    suite = unittest.TestSuite()
-    suite.addTest(TestCache('Test cache'))
-    suite.addTest(TestCacheNoPermission('Test cache no permission'))
-    return suite
+    ts: TestSuite = unittest.TestSuite()
+    ts.addTest(TestCache('Test cache'))
+    ts.addTest(TestCacheNoPermission('Test cache no permission'))
+    return ts
 
 
 if __name__ == '__main__':