code: 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
  
imports: from __future__ import print_function
  import datetime
  from io import StringIO
  import json
  import logging
  import time
  import warnings
  from typing import Optional
  from urllib.parse import quote
  import dateutil
  import numpy
  import pandas
  import requests
  from .None import shared, 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 .const import _BASE_URL_, _ROOT_URL_
label_desc: Use image node_id 10 for calling the TickerBase obj's underlying member method code with example usage: obj._fix_prices_sudden_change(df, interval, tz_exchange, change, correct_volume) and returns: df2, df, df, df, df, df, ranges, None, list, df