diff --git a/README.md b/README.md index 707912b..70cf571 100644 --- a/README.md +++ b/README.md @@ -596,6 +596,7 @@ help(ta.yf) * _Relative Strength Index_: **rsi** * _Relative Strength Xtra_: **rsx** * _Relative Vigor Index_: **rvgi** +* _Schaff Trend Cycle_: **stc** * _Slope_: **slope** * _SMI Ergodic_ **smi** * _Squeeze_: **squeeze** diff --git a/pandas_ta/__init__.py b/pandas_ta/__init__.py index 7972ef3..33ce5cf 100644 --- a/pandas_ta/__init__.py +++ b/pandas_ta/__init__.py @@ -47,7 +47,7 @@ Category = { "ao", "apo", "bias", "bop", "brar", "cci", "cfo", "cg", "cmo", "coppock", "er", "eri", "fisher", "inertia", "kdj", "kst", "macd", "mom", "pgo", "ppo", "psl", "pvo", "qqe", "roc", "rsi", "rsx", "rvgi", - "slope", "smi", "squeeze", "stoch", "stochrsi", "td_seq", "trix", "tsi", "uo", + "slope", "smi", "squeeze", "stc", "stoch", "stochrsi", "td_seq", "trix", "tsi", "uo", "willr" ], # Overlap diff --git a/pandas_ta/core.py b/pandas_ta/core.py index 2ca01f5..d447e44 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -1043,6 +1043,11 @@ class AnalysisIndicators(BasePandasObject): result = squeeze(high=high, low=low, close=close, bb_length=bb_length, bb_std=bb_std, kc_length=kc_length, kc_scalar=kc_scalar, mom_length=mom_length, mom_smooth=mom_smooth, use_tr=use_tr, offset=offset, **kwargs) return self._post_process(result, **kwargs) + def stc(self, ma1=None, ma2=None, osc=None, tclen=None, fast=None, slow=None, factor=None, offset=None, **kwargs): + close = self._get_column(kwargs.pop("close", "close")) + result = stc(close=close, ma1=ma1, ma2=ma2, osc=osc, tclen=tclen, fast=fast, slow=slow, factor=factor, offset=offset, **kwargs) + return self._post_process(result, **kwargs) + def stoch(self, fast_k=None, slow_k=None, slow_d=None, offset=None, **kwargs): high = self._get_column(kwargs.pop("high", "high")) low = self._get_column(kwargs.pop("low", "low")) diff --git a/pandas_ta/momentum/__init__.py b/pandas_ta/momentum/__init__.py index ded8f90..bda57d2 100644 --- a/pandas_ta/momentum/__init__.py +++ b/pandas_ta/momentum/__init__.py @@ -29,6 +29,7 @@ from .rvgi import rvgi from .slope import slope from .smi import smi from .squeeze import squeeze +from .stc import stc from .stoch import stoch from .stochrsi import stochrsi from .td_seq import td_seq diff --git a/pandas_ta/momentum/stc.py b/pandas_ta/momentum/stc.py new file mode 100644 index 0000000..dd7af63 --- /dev/null +++ b/pandas_ta/momentum/stc.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +from pandas import DataFrame, Series, concat +from pandas_ta.overlap import ema +from pandas_ta.utils import get_offset, verify_series, signals + + +def schaff_tc(close, XMAC, tclen, factor): + # ACTUAL Calculation part, which is shared between operation modes + # 1St : Stochastic of MACD + Value1 = XMAC.rolling(tclen).min() # min value in interval tclen + Value2 = XMAC.rolling(tclen).max() - Value1 # max value in interval tclen + + # ... : %Fast K of MACD + Frac1 = list(XMAC) + Frac1[0] = 0 + PF = list(XMAC) + PF[0] = 0 + for i in range(1, len(XMAC)): + if Value1[i] > 0: + Frac1[i] = ((XMAC[i] - Value1[i]) / Value2[i]) * 100 + else: + Frac1[i] = Frac1[i - 1] + # Smoothed Calculation for % Fast D of MACD + PF[i] = round(PF[i - 1] + (factor * (Frac1[i] - PF[i - 1])), 8) + + PF = Series(PF, index=close.index) + + # 2nd : Stochastic of smoothed Percent Fast D, 'PF', above + Value3 = PF.rolling(tclen).min() # min value in interval tclen + Value4 = PF.rolling(tclen).max() - Value3 # max value in interval tclen + # ... : % of Fast K of PF + Frac2 = list(XMAC) + Frac2[0] = 0 + PFF = list(XMAC) + PFF[0] = 0 + for i in range(1, len(XMAC)): + if Value4[i] > 0: + Frac2[i] = ((PF[i] - Value3[i]) / Value4[i]) * 100 + else: + Frac2[i] = Frac2[i - 1] + # Smoothed Calculation for % Fast D of MACD + PFF[i] = round(PFF[i - 1] + (factor * (Frac2[i] - PFF[i - 1])), 8) + + return [PFF, PF] + + +def stc(close, tclen=None, fast=None, slow=None, factor=None, offset=None, **kwargs): + """Indicator: Schaff Trend Cycle (STC)""" + # Validate arguments + close = verify_series(close) # close + tclen = int(tclen) if tclen and tclen > 0 else 10 + fast = int(fast) if fast and fast > 0 else 12 + slow = int(slow) if slow and slow > 0 else 26 + factor = float(factor) if factor and factor > 0 else 0.5 + if slow < fast: # mandatory condition, but might be confusing + fast, slow = slow, fast + offset = get_offset(offset) + + # kwargs allows for three more series (ma1, ma2 and osc) which can be passed here + # ma1 and ma2 input negate internal ema calculations, osc substitutes both ma's. + ma1 = kwargs.pop("ma1", False) + ma2 = kwargs.pop("ma2", False) + osc = kwargs.pop("osc", False) + + # 3 different modes of calculation.. + if isinstance(ma1, Series) and isinstance(ma2, Series) and not osc: + ma1 = verify_series(ma1) + ma2 = verify_series(ma2) + # Calculate Result based on external feeded series + XMAC = ma1 - ma2 + # invoke shared calculation + collect = schaff_tc(close, XMAC, tclen, factor) + + elif isinstance(osc, Series): + osc = verify_series(osc) + # Calculate Result based on feeded oscillator (should be ranging around 0 x-axis) + XMAC = osc + # invoke shared calculation + collect = schaff_tc(close, XMAC, tclen, factor) + + else: + # Calculate Result .. (traditionel/full) + # MACD line + fastma = ema(close, length=fast) + slowma = ema(close, length=slow) + XMAC = fastma - slowma + # invoke shared calculation + collect = schaff_tc(close, XMAC, tclen, factor) + + # Resulting Series + stc = Series(collect[0], index=close.index) + macd = Series(XMAC, index=close.index) + stoch = Series(collect[1], index=close.index) + + # Offset + if offset != 0: + stc = stc.shift(offset) + macd = macd.shift(offset) + stoch = stoch.shift(offset) + + # Handle fills + if "fillna" in kwargs: + stc.fillna(kwargs["fillna"], inplace=True) + macd.fillna(kwargs["fillna"], inplace=True) + stoch.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + stc.fillna(method=kwargs["fill_method"], inplace=True) + macd.fillna(method=kwargs["fill_method"], inplace=True) + stoch.fillna(method=kwargs["fill_method"], inplace=True) + + # Name and Categorize it + _props = f"_{tclen}_{fast}_{slow}_{factor}" + stc.name = f"STC{_props}" + macd.name = f"STCmacd{_props}" + stoch.name = f"STCstoch{_props}" + stc.category = macd.category = stoch.category ="momentum" + + # Prepare DataFrame to return + data = {stc.name: stc, macd.name: macd, stoch.name: stoch} + df = DataFrame(data) + df.name = f"STC{_props}" + df.category = stc.category + + return df + + +stc.__doc__ = \ +"""Schaff Trend Cycle (STC) + +The Schaff Trend Cycle is an evolution of the popular MACD incorportating two cascaded +stochastic calculations with additional smoothing. +The STC returns also the beginning MACD result as well as the result after the first stochastic +including its smoothing. This implementation has been extended for Pandas TA to +also allow for separatly feeding any other two moving Averages (as ma1 and ma2) or to skip this +to feed an oscillator (osc), based on which the Schaff Trend Cycle should be calculated. + +Feed external moving averages: +Internally calculation.. + stc = ta.stc(close=df["close"], tclen=stc_tclen, fast=ma1_interval, slow=ma2_interval, factor=stc_factor) +becomes.. + extMa1 = df.ta.zlma(close=df["close"], length=ma1_interval, append=True) + extMa2 = df.ta.ema(close=df["close"], length=ma2_interval, append=True) + stc = ta.stc(close=df["close"], tclen=stc_tclen, ma1=extMa1, ma2=extMa2, factor=stc_factor) + +The same goes for osc=, which allows the input of an externally calculated oscillator, overriding ma1 & ma2. + + +Sources: + Implemented by rengel8 based on work found here: + https://www.prorealcode.com/prorealtime-indicators/schaff-trend-cycle2/ + +Calculation: + STCmacd = Moving Average Convergance/Divergance or Oscillator + STCstoch = Intermediate Stochastic of MACD/Osc. + 2nd Stochastic including filtering with results in the + STC = Schaff Trend Cycle + +Args: + close (pd.Series): Series of 'close's, used for indexing Series, mandatory + tclen (int): SchaffTC Signal-Line length. Default: 10 (adjust to the half of cycle) + fast (int): The short period. Default: 12 + slow (int): The long period. Default: 26 + factor (float): smoothing factor for last stoch. calculation. Default: 0.5 + offset (int): How many periods to offset the result. Default: 0 + +Kwargs: + ma1: 1st moving average provided externally (mandatory in conjuction with ma2) + ma2: 2nd moving average provided externally (mandatory in conjuction with ma1) + osc: an externally feeded osillator + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + +Returns: + pd.DataFrame: stc, macd, stoch +"""