From 9d8a55f24225ff6aed3fb97c771b4da2b486e6a3 Mon Sep 17 00:00:00 2001 From: rengel8 <34138513+rengel8@users.noreply.github.com> Date: Thu, 8 Apr 2021 20:23:51 +0200 Subject: [PATCH] [ENH] Added Schaff Trend Cycle (STC) This request adds an extended version of STC, which has internally two EMAs, but can be feeded with any other two types of MAs and alternativly also with any oscillator type. --- README.md | 1 + pandas_ta/__init__.py | 2 +- pandas_ta/core.py | 5 + pandas_ta/momentum/__init__.py | 1 + pandas_ta/momentum/stc.py | 175 +++++++++++++++++++++++++++++++++ 5 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 pandas_ta/momentum/stc.py 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 +"""