From 71b9cbbf44c25379ccb29830669ca5d8afa7445f Mon Sep 17 00:00:00 2001 From: Kevin Johnson Date: Tue, 26 Feb 2019 08:54:12 -0800 Subject: [PATCH] volume indicators added --- MANIFEST | 2 + pandas_ta/volume.py | 815 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 817 insertions(+) create mode 100644 pandas_ta/volume.py diff --git a/MANIFEST b/MANIFEST index d05c84e..d92cd15 100644 --- a/MANIFEST +++ b/MANIFEST @@ -6,4 +6,6 @@ pandas_ta/__init__.py pandas_ta/core.py pandas_ta/overlap.py pandas_ta/performance.py +pandas_ta/statistics.py pandas_ta/utils.py +pandas_ta/volatility.py diff --git a/pandas_ta/volume.py b/pandas_ta/volume.py new file mode 100644 index 0000000..bcf8433 --- /dev/null +++ b/pandas_ta/volume.py @@ -0,0 +1,815 @@ +# -*- coding: utf-8 -*- +""" +.. module:: volume + :synopsis: Volume Indicators. + +.. moduleauthor:: Dario Lopez Padial (Bukosabino) + +""" +import numpy as np +import pandas as pd + +from .utils import get_drift, get_offset, signed_series, verify_series +from .momentum import roc +from .overlap import hl2, hlc3, ema + + +def ad(high, low, close, volume, open_=None, offset=None, **kwargs): + """Indicator: Accumulation/Distribution (AD)""" + # Validate Arguments + high = verify_series(high) + low = verify_series(low) + close = verify_series(close) + volume = verify_series(volume) + offset = get_offset(offset) + + # Calculate Result + if open_ is not None: + open_ = verify_series(open_) + ad = close - open_ # AD with Open + else: + ad = 2 * close - high - low # AD with High, Low, Close + + hl_range = high - low + ad *= volume / hl_range + ad = ad.cumsum() + + # Offset + if offset != 0: + ad = ad.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + ad.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + ad.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + ad.name = f"AD" + ad.category = 'volume' + + return ad + + +def adosc(high, low, close, volume, open_=None, fast=None, slow=None, offset=None, **kwargs): + """Indicator: Accumulation/Distribution Oscillator""" + # Validate Arguments + high = verify_series(high) + low = verify_series(low) + close = verify_series(close) + volume = verify_series(volume) + fast = int(fast) if fast and fast > 0 else 12 + slow = int(slow) if slow and slow > 0 else 26 + if slow < fast: + fast, slow = slow, fast + offset = get_offset(offset) + + # Calculate Result + ad_ = ad(high=high, low=low, close=close, volume=volume, open_=open_) + fast_ad = ema(close=ad_, length=fast) + slow_ad = ema(close=ad_, length=slow) + adosc = fast_ad - slow_ad + + # Offset + if offset != 0: + adosc = adosc.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + adosc.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + adosc.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + adosc.name = f"ADOSC_{fast}_{slow}" + adosc.category = 'volume' + + return adosc + + +def cmf(high, low, close, volume, open_=None, length=None, offset=None, **kwargs): + """Indicator: Chaikin Money Flow (CMF)""" + # Validate Arguments + high = verify_series(high) + low = verify_series(low) + close = verify_series(close) + volume = verify_series(volume) + length = int(length) if length and length > 0 else 20 + min_periods = int(kwargs['min_periods']) if 'min_periods' in kwargs and kwargs['min_periods'] is not None else length + offset = get_offset(offset) + + # Calculate Result + if open_ is not None: + open_ = verify_series(open_) + ad = close - open_ # AD with Open + else: + ad = 2 * close - high - low # AD with High, Low, Close + + hl_range = high - low + ad *= volume / hl_range + cmf = ad.rolling(length, min_periods=min_periods).sum() / volume.rolling(length, min_periods=min_periods).sum() + + # Offset + if offset != 0: + cmf = cmf.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + cmf.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + cmf.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + cmf.name = f"CMF_{length}" + cmf.category = 'volume' + + return cmf + + +def efi(close, volume, length=None, drift=None, mamode=None, offset=None, **kwargs): + """Indicator: Elder's Force Index (EFI)""" + # Validate arguments + close = verify_series(close) + volume = verify_series(volume) + length = int(length) if length and length > 0 else 13 + min_periods = int(kwargs['min_periods']) if 'min_periods' in kwargs and kwargs['min_periods'] is not None else length + drift = get_drift(drift) + mamode = mamode.lower() if mamode else None + offset = get_offset(offset) + + # Calculate Result + pv_diff = close.diff(drift) * volume + + if mamode == 'sma': + efi = pv_diff.rolling(length, min_periods=min_periods).mean() + else: + efi = pv_diff.ewm(span=length, min_periods=min_periods).mean() + + # Offset + if offset != 0: + efi = efi.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + efi.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + efi.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + efi.name = f"EFI_{length}" + efi.category = 'volume' + + return efi + + +def eom(high, low, close, volume, length=None, divisor=None, drift=None, offset=None, **kwargs): + """Indicator: Ease of Movement (EOM)""" + # Validate arguments + high = verify_series(high) + low = verify_series(low) + close = verify_series(close) + volume = verify_series(volume) + length = int(length) if length and length > 0 else 14 + min_periods = int(kwargs['min_periods']) if 'min_periods' in kwargs and kwargs['min_periods'] is not None else length + divisor = divisor if divisor and divisor > 0 else 100000000 + drift = get_drift(drift) + offset = get_offset(offset) + + # Calculate Result + hl_range = high - low + distance = hl2(high=high, low=low) - hl2(high=high.shift(drift), low=low.shift(drift)) + box_ratio = (volume / divisor) / hl_range + eom = distance / box_ratio + eom = eom.rolling(length, min_periods=min_periods).mean() + + # Offset + if offset != 0: + eom = eom.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + eom.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + eom.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + eom.name = f"EOM_{length}_{divisor}" + eom.category = 'volume' + + return eom + + +def mfi(high, low, close, volume, length=None, drift=None, offset=None, **kwargs): + """Indicator: Money Flow Index (MFI)""" + # Validate arguments + high = verify_series(high) + low = verify_series(low) + close = verify_series(close) + volume = verify_series(volume) + length = int(length) if length and length > 0 else 14 + drift = get_drift(drift) + offset = get_offset(offset) + + # Calculate Result + typical_price = hlc3(high=high, low=low, close=close) + raw_money_flow = typical_price * volume + + tdf = pd.DataFrame({'diff': 0, 'rmf': raw_money_flow, '+mf': 0, '-mf': 0}) + + tdf.loc[(typical_price.diff(drift) > 0), 'diff'] = 1 + tdf.loc[tdf['diff'] == 1, '+mf'] = raw_money_flow + + tdf.loc[(typical_price.diff(drift) < 0), 'diff'] = -1 + tdf.loc[tdf['diff'] == -1, '-mf'] = raw_money_flow + + psum = tdf['+mf'].rolling(length).sum() + nsum = tdf['-mf'].rolling(length).sum() + tdf['mr'] = psum / nsum + mfi = 100 * psum / (psum + nsum) + tdf['mfi'] = mfi + + # Offset + if offset != 0: + mfi = mfi.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + mfi.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + mfi.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + mfi.name = f"MFI_{length}" + mfi.category = 'momentum' + + return mfi + + +def nvi(close, volume, length=None, initial=None, offset=None, **kwargs): + """Indicator: Negative Volume Index (NVI)""" + # Validate arguments + close = verify_series(close) + volume = verify_series(volume) + length = int(length) if length and length > 0 else 1 + min_periods = int(kwargs['min_periods']) if 'min_periods' in kwargs and kwargs['min_periods'] is not None else length + initial = int(initial) if initial and initial > 0 else 1000 + offset = get_offset(offset) + + # Calculate Result + roc_ = roc(close=close) + signed_volume = signed_series(volume, initial=1) + nvi = signed_volume[signed_volume < 0].abs() * roc_ + nvi.fillna(0, inplace=True) + nvi.iloc[0]= initial + nvi = nvi.cumsum() + + # Offset + if offset != 0: + nvi = nvi.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + nvi.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + nvi.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + nvi.name = f"NVI_{length}" + nvi.category = 'volume' + + return nvi + + +def obv(close, volume, offset=None, **kwargs): + """Indicator: On Balance Volume (OBV)""" + # Validate arguments + close = verify_series(close) + volume = verify_series(volume) + offset = get_offset(offset) + + # Calculate Result + signed_volume = signed_series(close, initial=1) * volume + obv = signed_volume.cumsum() + + # Offset + if offset != 0: + obv = obv.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + obv.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + obv.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + obv.name = f"OBV" + obv.category = 'volume' + + return obv + + +def pvol(close, volume, signed=True, offset=None, **kwargs): + """Indicator: Price-Volume (PVOL)""" + # Validate arguments + close = verify_series(close) + volume = verify_series(volume) + offset = get_offset(offset) + + # Calculate Result + if signed: + pvol = signed_series(close, 1) * close * volume + else: + pvol = close * volume + + # Offset + if offset != 0: + pvol = pvol.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + pvol.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + pvol.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + pvol.name = f"PVOL" + pvol.category = 'volume' + + return pvol + + +def pvt(close, volume, drift=None, offset=None, **kwargs): + """Indicator: Price-Volume Trend (PVT)""" + # Validate arguments + close = verify_series(close) + volume = verify_series(volume) + drift = get_drift(drift) + offset = get_offset(offset) + + # Calculate Result + pv = roc(close=close, length=drift) * volume + pvt = pv.cumsum() + + # Offset + if offset != 0: + pvt = pvt.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + pvt.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + pvt.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + pvt.name = f"PVT" + pvt.category = 'volume' + + return pvt + + +def vp(close, volume, width=None, **kwargs): + """Indicator: Volume Profile (VP)""" + # Validate arguments + close = verify_series(close) + volume = verify_series(volume) + width = int(width) if width and width > 0 else 10 + + # Setup + signed_volume = signed_series(volume, initial=1) + pos_volume = signed_volume[signed_volume > 0] * volume + neg_volume = signed_volume[signed_volume < 0] * -volume + vp = pd.concat([close, pos_volume, neg_volume], axis=1) + + close_col = f"{vp.columns[0]}" + high_price_col = f"high_{close_col}" + low_price_col = f"low_{close_col}" + mean_price_col = f"mean_{close_col}" + mid_price_col = f"mid_{close_col}" + + volume_col = f"{vp.columns[1]}" + pos_volume_col = f"pos_{volume_col}" + neg_volume_col = f"neg_{volume_col}" + total_volume_col = f"total_{volume_col}" + vp.columns = [close_col, pos_volume_col, neg_volume_col] + + # sort_close: Sort by close before splitting into ranges. Default: False + # If False, it sorts by date index or chronological versus by price + if kwargs.pop('sort_close', False): + vp.sort_values(by=[close_col], inplace=True) + + # Calculate Result + vp_ranges = np.array_split(vp, width) + result = ({ + low_price_col: r[close_col].min(), + mean_price_col: r[close_col].mean(), + high_price_col: r[close_col].max(), + pos_volume_col: r[pos_volume_col].sum(), + neg_volume_col: r[neg_volume_col].sum(), + } for r in vp_ranges) + vpdf = pd.DataFrame(result) + vpdf[total_volume_col] = vpdf[pos_volume_col] + vpdf[neg_volume_col] + + # Handle fills + if 'fillna' in kwargs: + vpdf.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + vpdf.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + vpdf.name = f"VP" + vpdf.category = 'volume' + + return vpdf + + + +# Volume Documentation +ad.__doc__ = \ +"""Accumulation/Distribution (AD) + +Accumulation/Distribution indicator utilizes the relative position +of the close to it's High-Low range with volume. Then it is cumulated. + +Sources: + https://www.tradingtechnologies.com/help/x-study/technical-indicator-definitions/accumulationdistribution-ad/ + +Calculation: + CUM = Cumulative Sum + if 'open': + AD = close - open + else: + AD = 2 * close - high - low + + hl_range = high - low + AD = AD * volume / hl_range + AD = CUM(AD) + +Args: + high (pd.Series): Series of 'high's + low (pd.Series): Series of 'low's + close (pd.Series): Series of 'close's + volume (pd.Series): Series of 'volume's + open (pd.Series): Series of 'open's + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + + +adosc.__doc__ = \ +"""Accumulation/Distribution Oscillator or Chaikin Oscillator + +Accumulation/Distribution Oscillator indicator utilizes +Accumulation/Distribution and treats it similarily to MACD +or APO. + +Sources: + https://www.investopedia.com/articles/active-trading/031914/understanding-chaikin-oscillator.asp + +Calculation: + Default Inputs: + fast=12, slow=26 + AD = Accum/Dist + ad = AD(high, low, close, open) + fast_ad = EMA(ad, fast) + slow_ad = EMA(ad, slow) + ADOSC = fast_ad - slow_ad + +Args: + high (pd.Series): Series of 'high's + low (pd.Series): Series of 'low's + close (pd.Series): Series of 'close's + open (pd.Series): Series of 'open's + volume (pd.Series): Series of 'volume's + fast (int): The short period. Default: 12 + slow (int): The long period. Default: 26 + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + + +cmf.__doc__ = \ +"""Chaikin Money Flow (CMF) + +Chailin Money Flow measures the amount of money flow volume over a specific +period in conjunction with Accumulation/Distribution. + +Sources: + https://www.tradingview.com/wiki/Chaikin_Money_Flow_(CMF) + https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:chaikin_money_flow_cmf + +Calculation: + Default Inputs: + length=20 + if 'open': + ad = close - open + else: + ad = 2 * close - high - low + + hl_range = high - low + ad = ad * volume / hl_range + CMF = SUM(ad, length) / SUM(volume, length) + +Args: + high (pd.Series): Series of 'high's + low (pd.Series): Series of 'low's + close (pd.Series): Series of 'close's + open (pd.Series): Series of 'open's + volume (pd.Series): Series of 'volume's + length (int): The short period. Default: 20 + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + + +efi.__doc__ = \ +"""Elder's Force Index (EFI) + +Elder's Force Index measures the power behind a price movement using price +and volume as well as potential reversals and price corrections. + +Sources: + https://www.tradingview.com/wiki/Elder%27s_Force_Index_(EFI) + https://www.motivewave.com/studies/elders_force_index.htm + +Calculation: + Default Inputs: + length=20, drift=1, mamode=None + EMA = Exponential Moving Average + SMA = Simple Moving Average + + pv_diff = close.diff(drift) * volume + if mamode == 'sma': + EFI = SMA(pv_diff, length) + else: + EFI = EMA(pv_diff, length) + +Args: + close (pd.Series): Series of 'close's + volume (pd.Series): Series of 'volume's + length (int): The short period. Default: 13 + drift (int): The diff period. Default: 1 + mamode (str): Two options: None or 'sma'. Default: None + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + + +eom.__doc__ = \ +"""Ease of Movement (EOM) + +Ease of Movement is a volume based oscillator that is designed to measure the +relationship between price and volume flucuating across a zero line. + +Sources: + https://www.tradingview.com/wiki/Ease_of_Movement_(EOM) + https://www.motivewave.com/studies/ease_of_movement.htm + https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:ease_of_movement_emv + +Calculation: + Default Inputs: + length=14, divisor=100000000, drift=1 + SMA = Simple Moving Average + hl_range = high - low + distance = 0.5 * (high - high.shift(drift) + low - low.shift(drift)) + box_ratio = (volume / divisor) / hl_range + eom = distance / box_ratio + EOM = SMA(eom, length) + +Args: + high (pd.Series): Series of 'high's + low (pd.Series): Series of 'low's + close (pd.Series): Series of 'close's + volume (pd.Series): Series of 'volume's + length (int): The short period. Default: 14 + drift (int): The diff period. Default: 1 + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + + +mfi.__doc__ = \ +"""Money Flow Index (MFI) + +Money Flow Index is an oscillator indicator that is used to measure buying and +selling pressure by utilizing both price and volume. + +Sources: + https://www.tradingview.com/wiki/Money_Flow_(MFI) + +Calculation: + Default Inputs: + length=14, drift=1 + tp = typical_price = hlc3 = (high + low + close) / 3 + rmf = raw_money_flow = tp * volume + + pmf = pos_money_flow = SUM(rmf, length) if tp.diff(drift) > 0 else 0 + nmf = neg_money_flow = SUM(rmf, length) if tp.diff(drift) < 0 else 0 + + MFR = money_flow_ratio = pmf / nmf + MFI = money_flow_index = 100 * pmf / (pmf + nmf) + +Args: + high (pd.Series): Series of 'high's + low (pd.Series): Series of 'low's + close (pd.Series): Series of 'close's + volume (pd.Series): Series of 'volume's + length (int): The sum period. Default: 14 + drift (int): The difference period. Default: 1 + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + + +nvi.__doc__ = \ +"""Negative Volume Index (NVI) + +The Negative Volume Index is a cumulative indicator that uses volume change in +an attempt to identify where smart money is active. + +Sources: + https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:negative_volume_inde + https://www.motivewave.com/studies/negative_volume_index.htm + +Calculation: + Default Inputs: + length=20, initial=1000 + ROC = Rate of Change + + roc = ROC(close, length) + signed_volume = signed_series(volume, initial=1) + nvi = signed_volume[signed_volume < 0].abs() * roc_ + nvi.fillna(0, inplace=True) + nvi.iloc[0]= initial + nvi = nvi.cumsum() + +Args: + close (pd.Series): Series of 'close's + volume (pd.Series): Series of 'volume's + length (int): The short period. Default: 13 + initial (int): The short period. Default: 1000 + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + + +obv.__doc__ = \ +"""On Balance Volume (OBV) + +On Balance Volume is a cumulative indicator to measure buying and selling +pressure. + +Sources: + https://www.tradingview.com/wiki/On_Balance_Volume_(OBV) + https://www.tradingtechnologies.com/help/x-study/technical-indicator-definitions/on-balance-volume-obv/ + https://www.motivewave.com/studies/on_balance_volume.htm + +Calculation: + signed_volume = signed_series(close, initial=1) * volume + obv = signed_volume.cumsum() + +Args: + close (pd.Series): Series of 'close's + volume (pd.Series): Series of 'volume's + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + + +pvol.__doc__ = \ +"""Price-Volume (PVOL) + +Returns a series of the product of price and volume. + +Calculation: + if signed: + pvol = signed_series(close, 1) * close * volume + else: + pvol = close * volume + +Args: + close (pd.Series): Series of 'close's + volume (pd.Series): Series of 'volume's + signed (bool): Keeps the sign of the difference in 'close's. Default: True + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + + +pvt.__doc__ = \ +"""Price-Volume Trend (PVT) + +The Price-Volume Trend utilizes the Rate of Change with volume to +and it's cumulative values to determine money flow. + +Sources: + https://www.tradingview.com/wiki/Price_Volume_Trend_(PVT) + +Calculation: + Default Inputs: + drift=1 + ROC = Rate of Change + pv = ROC(close, drift) * volume + PVT = pv.cumsum() + +Args: + close (pd.Series): Series of 'close's + volume (pd.Series): Series of 'volume's + drift (int): The diff period. Default: 1 + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.Series: New feature generated. +""" + +vp.__doc__ = \ +"""Volume Profile (VP) + +Calculates the Volume Profile by slicing price into ranges. Note: Value Area is not calculated. + +Sources: + https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:volume_by_price + https://www.tradingview.com/wiki/Volume_Profile + http://www.ranchodinero.com/volume-tpo-essentials/ + https://www.tradingtechnologies.com/blog/2013/05/15/volume-at-price/ + +Calculation: + Default Inputs: + width=10 + + vp = pd.concat([close, pos_volume, neg_volume], axis=1) + vp_ranges = np.array_split(vp, width) + result = ({high_close, low_close, mean_close, neg_volume, pos_volume} foreach range in vp_ranges) + vpdf = pd.DataFrame(result) + vpdf['total_volume'] = vpdf['pos_volume'] + vpdf['neg_volume'] + +Args: + close (pd.Series): Series of 'close's + volume (pd.Series): Series of 'volume's + width (int): How many ranges to distrubute price into. Default: 10 + +Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + sort_close (value, optional): Whether to sort by close before splitting into ranges. Default: False + +Returns: + pd.DataFrame: New feature generated. +""" \ No newline at end of file