Repository: freqtrade/freqtrade

Issue:
Update parts of qtpylib included
The indicators borrowed from the qtpylib (freqtrade/vendor/qtpylib/indicators.py) seems to be now pretty old and contain outdated codebase.

That project has recently implemented some improvements and serious bugfixes in the last versions. For example, the authors fixed errorneous implementation of the stochastic indicator. See history of changes in https://github.com/ranaroussi/qtpylib/commits/master/qtpylib/indicators.py

The authors of qtpylib, however, have recently moved to the usage of Apache License, while freqtrade is licensed with GPL v3.

It should not be a problem though, Apache v2 software can be included into projects licensed with gpl v3, they are compatible this way. See description from Apache Software Foundation on compatibility with GPL for more info: http://www.apache.org/licenses/GPL-compatibility.html



Discussion:

Resolving patch:
diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py
--- a/freqtrade/vendor/qtpylib/indicators.py
+++ b/freqtrade/vendor/qtpylib/indicators.py
@@ -4,13 +4,13 @@
 # QTPyLib: Quantitative Trading Python Library
 # https://github.com/ranaroussi/qtpylib
 #
-# Copyright 2016 Ran Aroussi
+# Copyright 2016-2018 Ran Aroussi
 #
-# Licensed under the GNU Lesser General Public License, v3.0 (the "License");
+# Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at
 #
-#     https://www.gnu.org/licenses/lgpl-3.0.en.html
+#     http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,8 +19,8 @@
 # limitations under the License.
 #
 
-import sys
 import warnings
+import sys
 from datetime import datetime, timedelta
 
 import numpy as np
@@ -62,19 +62,20 @@ def func_wrapper(data, window, as_source=False):
 
 @numpy_rolling_series
 def numpy_rolling_mean(data, window, as_source=False):
-    return np.mean(numpy_rolling_window(data, window), -1)
+    return np.mean(numpy_rolling_window(data, window), axis=-1)
 
 
 @numpy_rolling_series
 def numpy_rolling_std(data, window, as_source=False):
-    return np.std(numpy_rolling_window(data, window), -1)
+    return np.std(numpy_rolling_window(data, window), axis=-1, ddof=1)
+
 
 # ---------------------------------------------
 
 
 def session(df, start='17:00', end='16:00'):
     """ remove previous globex day from df """
-    if len(df) == 0:
+    if df.empty:
         return df
 
     # get start/end/now as decimals
@@ -103,47 +104,47 @@ def session(df, start='17:00', end='16:00'):
 
     return df.copy()
 
-
 # ---------------------------------------------
 
+
 def heikinashi(bars):
     bars = bars.copy()
     bars['ha_close'] = (bars['open'] + bars['high'] +
                         bars['low'] + bars['close']) / 4
 
-    bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
-    bars.loc[:1, 'ha_open'] = bars['open'].values[0]
-    for x in range(2):
-        bars.loc[1:, 'ha_open'] = (
-            (bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
+    # ha open
+    bars.at[0, 'ha_open'] = (bars.at[0, 'open'] + bars.at[0, 'close']) / 2
+    for i in range(1, len(bars)):
+        bars.at[i, 'ha_open'] = (bars.at[i - 1, 'ha_open'] + bars.at[i - 1, 'ha_close']) / 2
 
     bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
     bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
 
-    return pd.DataFrame(
-        index=bars.index,
-        data={
-            'open': bars['ha_open'],
-            'high': bars['ha_high'],
-            'low': bars['ha_low'],
-            'close': bars['ha_close']})
-
+    return pd.DataFrame(index=bars.index,
+                        data={'open': bars['ha_open'],
+                              'high': bars['ha_high'],
+                              'low': bars['ha_low'],
+                              'close': bars['ha_close']})
 
 # ---------------------------------------------
 
-def tdi(series, rsi_len=13, bollinger_len=34, rsi_smoothing=2,
-        rsi_signal_len=7, bollinger_std=1.6185):
-    rsi_series = rsi(series, rsi_len)
-    bb_series = bollinger_bands(rsi_series, bollinger_len, bollinger_std)
-    signal = sma(rsi_series, rsi_signal_len)
-    rsi_series = sma(rsi_series, rsi_smoothing)
+
+def tdi(series, rsi_lookback=13, rsi_smooth_len=2,
+        rsi_signal_len=7, bb_lookback=34, bb_std=1.6185):
+
+    rsi_data = rsi(series, rsi_lookback)
+    rsi_smooth = sma(rsi_data, rsi_smooth_len)
+    rsi_signal = sma(rsi_data, rsi_signal_len)
+
+    bb_series = bollinger_bands(rsi_data, bb_lookback, bb_std)
 
     return pd.DataFrame(index=series.index, data={
-        "rsi": rsi_series,
-        "signal": signal,
-        "bbupper": bb_series['upper'],
-        "bblower": bb_series['lower'],
-        "bbmid": bb_series['mid']
+        "rsi": rsi_data,
+        "rsi_signal": rsi_signal,
+        "rsi_smooth": rsi_smooth,
+        "rsi_bb_upper": bb_series['upper'],
+        "rsi_bb_lower": bb_series['lower'],
+        "rsi_bb_mid": bb_series['mid']
     })
 
 # ---------------------------------------------
@@ -163,8 +164,8 @@ def awesome_oscillator(df, weighted=False, fast=5, slow=34):
 
 # ---------------------------------------------
 
-def nans(len=1):
-    mtx = np.empty(len)
+def nans(length=1):
+    mtx = np.empty(length)
     mtx[:] = np.nan
     return mtx
 
@@ -222,7 +223,7 @@ def crossed(series1, series2, direction=None):
     if isinstance(series1, np.ndarray):
         series1 = pd.Series(series1)
 
-    if isinstance(series2, int) or isinstance(series2, float) or isinstance(series2, np.ndarray):
+    if isinstance(series2, (float, int, np.ndarray)):
         series2 = pd.Series(index=series1.index, data=series2)
 
     if direction is None or direction == "above":
@@ -256,7 +257,7 @@ def rolling_std(series, window=200, min_periods=None):
     else:
         try:
             return series.rolling(window=window, min_periods=min_periods).std()
-        except BaseException:
+        except Exception as e:  # noqa: F841
             return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
 
 # ---------------------------------------------
@@ -269,7 +270,7 @@ def rolling_mean(series, window=200, min_periods=None):
     else:
         try:
             return series.rolling(window=window, min_periods=min_periods).mean()
-        except BaseException:
+        except Exception as e:  # noqa: F841
             return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
 
 # ---------------------------------------------
@@ -279,7 +280,7 @@ def rolling_min(series, window=14, min_periods=None):
     min_periods = window if min_periods is None else min_periods
     try:
         return series.rolling(window=window, min_periods=min_periods).min()
-    except BaseException:
+    except Exception as e:  # noqa: F841
         return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
 
 
@@ -289,7 +290,7 @@ def rolling_max(series, window=14, min_periods=None):
     min_periods = window if min_periods is None else min_periods
     try:
         return series.rolling(window=window, min_periods=min_periods).min()
-    except BaseException:
+    except Exception as e:  # noqa: F841
         return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
 
 
@@ -299,16 +300,17 @@ def rolling_weighted_mean(series, window=200, min_periods=None):
     min_periods = window if min_periods is None else min_periods
     try:
         return series.ewm(span=window, min_periods=min_periods).mean()
-    except BaseException:
+    except Exception as e:  # noqa: F841
         return pd.ewma(series, span=window, min_periods=min_periods)
 
 
 # ---------------------------------------------
 
-def hull_moving_average(series, window=200):
-    wma = (2 * rolling_weighted_mean(series, window=window / 2)) - \
-        rolling_weighted_mean(series, window=window)
-    return rolling_weighted_mean(wma, window=np.sqrt(window))
+def hull_moving_average(series, window=200, min_periods=None):
+    min_periods = window if min_periods is None else min_periods
+    ma = (2 * rolling_weighted_mean(series, window / 2, min_periods)) - \
+        rolling_weighted_mean(series, window, min_periods)
+    return rolling_weighted_mean(ma, np.sqrt(window), min_periods)
 
 
 # ---------------------------------------------
@@ -325,8 +327,8 @@ def wma(series, window=200, min_periods=None):
 
 # ---------------------------------------------
 
-def hma(series, window=200):
-    return hull_moving_average(series, window=window)
+def hma(series, window=200, min_periods=None):
+    return hull_moving_average(series, window=window, min_periods=min_periods)
 
 
 # ---------------------------------------------
@@ -361,7 +363,8 @@ def rolling_vwap(bars, window=200, min_periods=None):
                                       min_periods=min_periods).sum()
     right = volume.rolling(window=window, min_periods=min_periods).sum()
 
-    return pd.Series(index=bars.index, data=(left / right))
+    return pd.Series(index=bars.index, data=(left / right)
+                     ).replace([np.inf, -np.inf], float('NaN')).ffill()
 
 
 # ---------------------------------------------
@@ -370,6 +373,7 @@ def rsi(series, window=14):
     """
     compute the n period relative strength indicator
     """
+
     # 100-(100/relative_strength)
     deltas = np.diff(series)
     seed = deltas[:window + 1]
@@ -406,13 +410,13 @@ def macd(series, fast=3, slow=10, smooth=16):
     using a fast and slow exponential moving avg'
     return value is emaslow, emafast, macd which are len(x) arrays
     """
-    macd = rolling_weighted_mean(series, window=fast) - \
+    macd_line = rolling_weighted_mean(series, window=fast) - \
         rolling_weighted_mean(series, window=slow)
-    signal = rolling_weighted_mean(macd, window=smooth)
-    histogram = macd - signal
-    # return macd, signal, histogram
+    signal = rolling_weighted_mean(macd_line, window=smooth)
+    histogram = macd_line - signal
+    # return macd_line, signal, histogram
     return pd.DataFrame(index=series.index, data={
-        'macd': macd.values,
+        'macd': macd_line.values,
         'signal': signal.values,
         'histogram': histogram.values
     })
@@ -421,14 +425,14 @@ def macd(series, fast=3, slow=10, smooth=16):
 # ---------------------------------------------
 
 def bollinger_bands(series, window=20, stds=2):
-    sma = rolling_mean(series, window=window)
-    std = rolling_std(series, window=window)
-    upper = sma + std * stds
-    lower = sma - std * stds
+    ma = rolling_mean(series, window=window, min_periods=1)
+    std = rolling_std(series, window=window, min_periods=1)
+    upper = ma + std * stds
+    lower = ma - std * stds
 
     return pd.DataFrame(index=series.index, data={
         'upper': upper,
-        'mid': sma,
+        'mid': ma,
         'lower': lower
     })
 
@@ -454,7 +458,7 @@ def returns(series):
     try:
         res = (series / series.shift(1) -
                1).replace([np.inf, -np.inf], float('NaN'))
-    except BaseException:
+    except Exception as e:  # noqa: F841
         res = nans(len(series))
 
     return pd.Series(index=series.index, data=res)
@@ -466,7 +470,7 @@ def log_returns(series):
     try:
         res = np.log(series / series.shift(1)
                      ).replace([np.inf, -np.inf], float('NaN'))
-    except BaseException:
+    except Exception as e:  # noqa: F841
         res = nans(len(series))
 
     return pd.Series(index=series.index, data=res)
@@ -479,7 +483,7 @@ def implied_volatility(series, window=252):
         logret = np.log(series / series.shift(1)
                         ).replace([np.inf, -np.inf], float('NaN'))
         res = numpy_rolling_std(logret, window) * np.sqrt(window)
-    except BaseException:
+    except Exception as e:  # noqa: F841
         res = nans(len(series))
 
     return pd.Series(index=series.index, data=res)
@@ -530,32 +534,55 @@ def stoch(df, window=14, d=3, k=3, fast=False):
     compute the n period relative strength indicator
     http://excelta.blogspot.co.il/2013/09/stochastic-oscillator-technical.html
     """
-    highs_ma = pd.concat([df['high'].shift(i)
-                          for i in np.arange(window)], 1).apply(list, 1)
-    highs_ma = highs_ma.T.max().T
 
-    lows_ma = pd.concat([df['low'].shift(i)
-                         for i in np.arange(window)], 1).apply(list, 1)
-    lows_ma = lows_ma.T.min().T
+    my_df = pd.DataFrame(index=df.index)
 
-    fast_k = ((df['close'] - lows_ma) / (highs_ma - lows_ma)) * 100
-    fast_d = numpy_rolling_mean(fast_k, d)
+    my_df['rolling_max'] = df['high'].rolling(window).max()
+    my_df['rolling_min'] = df['low'].rolling(window).min()
+
+    my_df['fast_k'] = (
+        100 * (df['close'] - my_df['rolling_min']) /
+        (my_df['rolling_max'] - my_df['rolling_min'])
+    )
+    my_df['fast_d'] = my_df['fast_k'].rolling(d).mean()
 
     if fast:
-        data = {
-            'k': fast_k,
-            'd': fast_d
-        }
+        return my_df.loc[:, ['fast_k', 'fast_d']]
 
-    else:
-        slow_k = numpy_rolling_mean(fast_k, k)
-        slow_d = numpy_rolling_mean(slow_k, d)
-        data = {
-            'k': slow_k,
-            'd': slow_d
-        }
+    my_df['slow_k'] = my_df['fast_k'].rolling(k).mean()
+    my_df['slow_d'] = my_df['slow_k'].rolling(d).mean()
 
-    return pd.DataFrame(index=df.index, data=data)
+    return my_df.loc[:, ['slow_k', 'slow_d']]
+
+# ---------------------------------------------
+
+
+def zlma(series, window=20, min_periods=None, kind="ema"):
+    """
+    John Ehlers' Zero lag (exponential) moving average
+    https://en.wikipedia.org/wiki/Zero_lag_exponential_moving_average
+    """
+    min_periods = window if min_periods is None else min_periods
+
+    lag = (window - 1) // 2
+    series = 2 * series - series.shift(lag)
+    if kind in ['ewm', 'ema']:
+        return wma(series, lag, min_periods)
+    elif kind == "hma":
+        return hma(series, lag, min_periods)
+    return sma(series, lag, min_periods)
+
+
+def zlema(series, window, min_periods=None):
+    return zlma(series, window, min_periods, kind="ema")
+
+
+def zlsma(series, window, min_periods=None):
+    return zlma(series, window, min_periods, kind="sma")
+
+
+def zlhma(series, window, min_periods=None):
+    return zlma(series, window, min_periods, kind="hma")
 
 # ---------------------------------------------
 
@@ -571,13 +598,13 @@ def zscore(bars, window=20, stds=1, col='close'):
 
 def pvt(bars):
     """ Price Volume Trend """
-    pvt = ((bars['close'] - bars['close'].shift(1)) /
-           bars['close'].shift(1)) * bars['volume']
-    return pvt.cumsum()
-
+    trend = ((bars['close'] - bars['close'].shift(1)) /
+             bars['close'].shift(1)) * bars['volume']
+    return trend.cumsum()
 
 # =============================================
 
+
 PandasObject.session = session
 PandasObject.atr = atr
 PandasObject.bollinger_bands = bollinger_bands
@@ -613,4 +640,11 @@ def pvt(bars):
 
 PandasObject.sma = sma
 PandasObject.wma = wma
+PandasObject.ema = wma
 PandasObject.hma = hma
+
+PandasObject.zlsma = zlsma
+PandasObject.zlwma = zlema
+PandasObject.zlema = zlema
+PandasObject.zlhma = zlhma
+PandasObject.zlma = zlma


Test patch:
diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py
--- a/freqtrade/tests/optimize/test_backtesting.py
+++ b/freqtrade/tests/optimize/test_backtesting.py
@@ -33,7 +33,7 @@ def get_args(args) -> List[str]:
 def trim_dictlist(dict_list, num):
     new = {}
     for pair, pair_data in dict_list.items():
-        new[pair] = pair_data[num:]
+        new[pair] = pair_data[num:].reset_index()
     return new
 
 
@@ -708,7 +708,7 @@ def _trend_alternate_hold(dataframe=None, metadata=None):
     data = trim_dictlist(data, -500)
 
     # Remove data for one pair from the beginning of the data
-    data[pair] = data[pair][tres:]
+    data[pair] = data[pair][tres:].reset_index()
     # We need to enable sell-signal - otherwise it sells on ROI!!
     default_conf['experimental'] = {"use_sell_signal": True}
     default_conf['ticker_interval'] = '5m'