diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 index 19cac2d5..3111bbe6 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:latest +FROM python:3.9 # TA-lib is required by the python TA-lib wrapper. This provides analysis. COPY lib/ta-lib-0.4.0-src.tar.gz /tmp/ta-lib-0.4.0-src.tar.gz @@ -14,6 +14,9 @@ COPY ./app /app WORKDIR /app +RUN apt update && apt upgrade -y +RUN apt install -y fail2ban + RUN pip install --upgrade pip RUN pip install -r requirements-step-1.txt RUN pip install -r requirements-step-2.txt diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index db759897..ed0fe9c8 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Development branch to testing new features. This develop version has a lot of im ## Notable Changes - It creates candle bar charts with MAs, RSI and MACD. These images can be sent as part of a Telegram notification or a Webhook call. - It allows to include prices as part of the notification message. -- New configuration to easily add many coins. Check bellow for "all_pairs". +- New configuration to easily add many coins. Check bellow for "all_pairs". - New config var to use a custom "indicator_label" for each configured indicator and crossovers. Mainly useful for std_crossover. - New indicator iiv (Increase In Volume) to try to identify a pump/dump. - New indicator MA Ribbon @@ -16,7 +16,14 @@ Development branch to testing new features. This develop version has a lot of im - New indicator StochRSI Cross - New indicator Sqzmon - Squeeze Momentum Indicator - New option to customize ichimoku strategies and added chikou span - +- Uptrend evaluation for a signal, it compares two signal candles to determine uptrend +- Changed Bollinger bands indicator type from informant to indicator to send message status to telegram +- Adde Bollinger bands width to Bollinger bands indicator +- New Indicator NATR +- New Indicator ROC +- New indicator IFish Stoch +- Added pandas-ta and dependencies in requirements +- New indicator IIP (price) ## Installing And Running The commands listed below are intended to be run in a terminal. @@ -25,13 +32,17 @@ Be sure you have git installed in your system. 1. Clone this repo `git clone https://github.com/w1ld3r/crypto-signal.git` -1. Enter to cripto-signal folder `cd crypto-signal` +2. Enter to cripto-signal folder `cd crypto-signal` -1. Switch to develop branch `git checkout develop` +3. Switch to develop branch `git checkout develop` -1. Create a config.yml file and put it into "app" folder. +4. Create a config.yml file and put it into "app" folder. + +5. Build and run the docker container: `docker-compose up --build` -1. Build and run the docker container: `docker-compose up --build` +6. For testing and debugging run docker with "-t" option `docker run --rm -ti -v $PWD/app:/app crypto-signal_app:latest` + +7. For production run in daemon mode using "-d" option `docker run --rm -di -v $PWD/app:/app crypto-signal_app:latest` ### Configuring config.yml @@ -548,7 +559,26 @@ indicators: signal: - close ``` +#### CCI +It is a volatility and momentum indicator which capitalizes on the tendency for price to break out strongly after consolidating in a tight trading range. + +``` +indicators: + cci: + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: always + cold: 100 + hot: -100 + signal: + - cci + hot_label: 'Momentum Increasing or OverSold' + cold_label: 'Momentum Decreasing or OverBought' + indicator_label: 'CCI 4h' + mute_cold: false +``` #### Chart images on webhook The config for a webhook notifier is the same as original CryptoSignal, no changes here, BUT the data sent in the request is completely different. diff --git a/app/.gitignore b/app/.gitignore old mode 100644 new mode 100755 diff --git a/app/__init__.py b/app/__init__.py old mode 100644 new mode 100755 diff --git a/app/analysis.py b/app/analysis.py old mode 100644 new mode 100755 index 2a7bc799..1c683e8e --- a/app/analysis.py +++ b/app/analysis.py @@ -13,12 +13,11 @@ from analyzers.informants import * -class StrategyAnalyzer(): - """Contains all the methods required for analyzing strategies. - """ +class StrategyAnalyzer: + """Contains all the methods required for analyzing strategies.""" def __init__(self): - """Initializes StrategyAnalyzer class """ + """Initializes StrategyAnalyzer class""" self.logger = structlog.get_logger() def indicator_dispatcher(self): @@ -29,25 +28,33 @@ def indicator_dispatcher(self): """ dispatcher = { - 'candle_recognition': candle_recognition.Candle_recognition().analyze, - 'aroon_oscillator': aroon_oscillator.Aroon_oscillator().analyze, - 'klinger_oscillator': klinger_oscillator.Klinger_oscillator().analyze, - 'adx': adx.Adx().analyze, - 'ichimoku': ichimoku.Ichimoku().analyze, - 'macd': macd.MACD().analyze, - 'rsi': rsi.RSI().analyze, - 'momentum': momentum.Momentum().analyze, - 'mfi': mfi.MFI().analyze, - 'stoch_rsi': stoch_rsi.StochasticRSI().analyze, - 'obv': obv.OBV().analyze, - 'iiv': iiv.IIV().analyze, - 'ma_ribbon': ma_ribbon.MARibbon().analyze, - 'ma_crossover': ma_crossover.MACrossover().analyze, - 'bollinger': bollinger.Bollinger().analyze, - 'bbp': bbp.BBP().analyze, - 'macd_cross': macd_cross.MACDCross().analyze, - 'stochrsi_cross': stochrsi_cross.StochRSICross().analyze, - 'sqzmom': sqzmom.SQZMOM().analyze + "candle_recognition": candle_recognition.Candle_recognition().analyze, + "aroon_oscillator": aroon_oscillator.Aroon_oscillator().analyze, + "klinger_oscillator": klinger_oscillator.Klinger_oscillator().analyze, + "adx": adx.Adx().analyze, + "ichimoku": ichimoku.Ichimoku().analyze, + "macd": macd.MACD().analyze, + "rsi": rsi.RSI().analyze, + "momentum": momentum.Momentum().analyze, + "cci": cci.CCI().analyze, + "mfi": mfi.MFI().analyze, + "stoch_rsi": stoch_rsi.StochasticRSI().analyze, + "stoch_hma_avg_rsi": stoch_hma_avg_rsi.StochHMAAVGRSI().analyze, + "stoch_hma": stoch_hma.StochHMA().analyze, + "obv": obv.OBV().analyze, + "iiv": iiv.IIV().analyze, + "ma_ribbon": ma_ribbon.MARibbon().analyze, + "ma_crossover": ma_crossover.MACrossover().analyze, + "bollinger": bollinger.Bollinger().analyze, + "bbp": bbp.BBP().analyze, + "macd_cross": macd_cross.MACDCross().analyze, + "stochrsi_cross": stochrsi_cross.StochRSICross().analyze, + "sqzmom": sqzmom.SQZMOM().analyze, + "natr": natr.NATR().analyze, + "bollinger_bands": bollinger_bands.Bollinger().analyze, + "roc": roc.ROC().analyze, + "ifish_stoch": ifish_stoch.IFISH_STOCH().analyze, + "iip": iip.IIP().analyze, } return dispatcher @@ -60,12 +67,12 @@ def informant_dispatcher(self): """ dispatcher = { - 'sma': sma.SMA().analyze, - 'ema': ema.EMA().analyze, - 'vwap': vwap.VWAP().analyze, - 'bollinger_bands': bollinger_bands.Bollinger().analyze, - 'ohlcv': ohlcv.OHLCV().analyze, - 'lrsi': lrsi.LRSI().analyze + "sma": sma.SMA().analyze, + "ema": ema.EMA().analyze, + "vwap": vwap.VWAP().analyze, + "ohlcv": ohlcv.OHLCV().analyze, + "lrsi": lrsi.LRSI().analyze, + "hma": hma.HMA().analyze, } return dispatcher @@ -77,8 +84,17 @@ def crossover_dispatcher(self): dictionary: A dictionary of functions to serve as a dynamic crossover selector. """ - dispatcher = { - 'std_crossover': crossover.CrossOver().analyze - } + dispatcher = {"std_crossover": crossover.CrossOver().analyze} + + return dispatcher + + def uptrend_dispatcher(self): + """Returns a pandas.DataFrame for dynamic uptrend selector + + Returns: + dictionary: A dictionary of functions to serve as a dynamic uptrend selector. + """ + + dispatcher = {"std_uptrend": uptrend.UpTrend().analyze} return dispatcher diff --git a/app/analyzers/__init__.py b/app/analyzers/__init__.py old mode 100644 new mode 100755 index 38857811..3fad1efe --- a/app/analyzers/__init__.py +++ b/app/analyzers/__init__.py @@ -1 +1 @@ -__all__ = ['crossover'] +__all__ = ['crossover', 'uptrend'] diff --git a/app/analyzers/crossover.py b/app/analyzers/crossover.py old mode 100644 new mode 100755 index 477282e6..f37740f9 --- a/app/analyzers/crossover.py +++ b/app/analyzers/crossover.py @@ -1,30 +1,23 @@ -""" Crossover analysis indicator -""" - -import numpy -import pandas -from talib import abstract - +import pandas as pd from analyzers.utils import IndicatorUtils - class CrossOver(IndicatorUtils): def analyze(self, key_indicator, key_signal, key_indicator_index, crossed_indicator, crossed_signal, crossed_indicator_index): """ Tests for key_indicator crossing over the crossed_indicator. Args: - key_indicator (pandas.DataFrame): A dataframe containing the results of the analysis + key_indicator (pd.DataFrame): A dataframe containing the results of the analysis for the selected key indicator. key_signal (str): The name of the key indicator. key_indicator_index (int): The configuration index of the key indicator to use. - crossed_indicator (pandas.DataFrame): A dataframe containing the results of the + crossed_indicator (pd.DataFrame): A dataframe containing the results of the analysis for the selected indicator to test for a cross. crossed_signal (str): The name of the indicator expecting to be crossed. crossed_indicator_index (int): The configuration index of the crossed indicator to use. Returns: - pandas.DataFrame: A dataframe containing the indicators and hot/cold values. + pd.DataFrame: A dataframe containing the indicators and hot/cold values. """ key_indicator_name = '{}_{}'.format(key_signal, key_indicator_index) @@ -39,10 +32,19 @@ def analyze(self, key_indicator, key_signal, key_indicator_index, column_indexed_name = '{}_{}'.format(column, crossed_indicator_index) new_crossed_indicator.rename(columns={column: column_indexed_name}, inplace=True) - combined_data = pandas.concat([new_key_indicator, new_crossed_indicator], axis=1) + combined_data = pd.concat([new_key_indicator, new_crossed_indicator], axis=1) combined_data.dropna(how='any', inplace=True) - combined_data['is_hot'] = combined_data[key_indicator_name] > combined_data[crossed_indicator_name] - combined_data['is_cold'] = combined_data[key_indicator_name] < combined_data[crossed_indicator_name] + # Check if the key indicator crossed above the crossed indicator + combined_data['is_hot'] = ( + (combined_data[key_indicator_name].shift() < combined_data[crossed_indicator_name].shift()) & + (combined_data[key_indicator_name] > combined_data[crossed_indicator_name]) + ) + + # Check if the key indicator crossed below the crossed indicator + combined_data['is_cold'] = ( + (combined_data[key_indicator_name].shift() > combined_data[crossed_indicator_name].shift()) & + (combined_data[key_indicator_name] < combined_data[crossed_indicator_name]) + ) return combined_data diff --git a/app/analyzers/indicators/__init__.py b/app/analyzers/indicators/__init__.py old mode 100644 new mode 100755 index 6a3fe7c5..081d6813 --- a/app/analyzers/indicators/__init__.py +++ b/app/analyzers/indicators/__init__.py @@ -1,21 +1,29 @@ __all__ = [ - 'candle_recognition', - 'aroon_oscillator', - 'klinger_oscillator', - 'adx', - 'ichimoku', - 'macd', - 'momentum', - 'rsi', - 'stoch_rsi', - 'mfi', - 'obv', - 'iiv', - 'ma_ribbon', - 'ma_crossover', - 'bollinger', - 'bbp', - 'macd_cross', - 'stochrsi_cross', - 'sqzmom' + "candle_recognition", + "aroon_oscillator", + "klinger_oscillator", + "adx", + "ichimoku", + "macd", + "momentum", + "cci", + "rsi", + "stoch_rsi", + "stoch_hma_avg_rsi", + "stoch_hma", + "mfi", + "obv", + "iiv", + "ma_ribbon", + "ma_crossover", + "bollinger", + "bbp", + "macd_cross", + "stochrsi_cross", + "sqzmom", + "natr", + "bollinger_bands", + "roc", + "ifish_stoch", + "iip", ] diff --git a/app/analyzers/indicators/adx.py b/app/analyzers/indicators/adx.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/aroon_oscillator.py b/app/analyzers/indicators/aroon_oscillator.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/bbp.py b/app/analyzers/indicators/bbp.py old mode 100644 new mode 100755 index c6feb37b..b6b14cf0 --- a/app/analyzers/indicators/bbp.py +++ b/app/analyzers/indicators/bbp.py @@ -7,13 +7,14 @@ import pandas from talib import BBANDS, abstract +import numpy as np from analyzers.utils import IndicatorUtils class BBP(IndicatorUtils): - def analyze(self, historical_data, signal=['bbp'], hot_thresh=0, cold_thresh=0.8, period_count=20, std_dev=2): + def analyze(self, historical_data, signal=['bbp'], hot_thresh=0.3, cold_thresh=0.8, period_count=20, std_dev=2): """Check when close price cross the Upper/Lower bands. Args: @@ -47,8 +48,8 @@ def analyze(self, historical_data, signal=['bbp'], hot_thresh=0, cold_thresh=0.8 bollinger['is_hot'] = False bollinger['is_cold'] = False + + bollinger['is_hot'] = bollinger['bbp'] <= hot_thresh + bollinger['is_cold'] = bollinger['bbp'] >= cold_thresh - bollinger['is_hot'].iloc[-1] = bollinger['bbp'].iloc[-2] <= hot_thresh and bollinger['bbp'].iloc[-2] < bollinger['bbp'].iloc[-1] - bollinger['is_cold'].iloc[-1] = bollinger['bbp'].iloc[-1] >= cold_thresh - - return bollinger + return bollinger \ No newline at end of file diff --git a/app/analyzers/indicators/bollinger.py b/app/analyzers/indicators/bollinger.py old mode 100644 new mode 100755 diff --git a/app/analyzers/informants/bollinger_bands.py b/app/analyzers/indicators/bollinger_bands.py old mode 100644 new mode 100755 similarity index 65% rename from app/analyzers/informants/bollinger_bands.py rename to app/analyzers/indicators/bollinger_bands.py index 17100a88..17ad95d7 --- a/app/analyzers/informants/bollinger_bands.py +++ b/app/analyzers/indicators/bollinger_bands.py @@ -11,7 +11,7 @@ class Bollinger(IndicatorUtils): - def analyze(self, historical_data, period_count=21): + def analyze(self, historical_data, signal=['bbwidth'], hot_thresh=None, cold_thresh=None, period_count=21): """Performs a bollinger band analysis on the historical data Args: @@ -28,7 +28,8 @@ def analyze(self, historical_data, period_count=21): bb_columns = { 'upperband': [numpy.nan] * dataframe.index.shape[0], 'middleband': [numpy.nan] * dataframe.index.shape[0], - 'lowerband': [numpy.nan] * dataframe.index.shape[0] + 'lowerband': [numpy.nan] * dataframe.index.shape[0], + 'bbwidth': [numpy.nan] * dataframe.index.shape[0] } bb_values = pandas.DataFrame( @@ -44,10 +45,13 @@ def analyze(self, historical_data, period_count=21): for index in range(period_count, bb_df_size): data_index = index - period_count - bb_values['lowerband'][index] = bb_data[0][data_index] - bb_values['middleband'][index] = bb_data[1][data_index] - bb_values['upperband'][index] = bb_data[2][data_index] - + bb_values['lowerband'].iloc[index] = bb_data[0][data_index] + bb_values['middleband'].iloc[index] = bb_data[1][data_index] + bb_values['upperband'].iloc[index] = bb_data[2][data_index] + bb_values['bbwidth'].iloc[index] = (bb_data[2][data_index] - bb_data[0][data_index]) / bb_data[1][data_index] + + bb_values['is_hot'] = True + bb_values['is_cold'] = False bb_values.dropna(how='all', inplace=True) return bb_values diff --git a/app/analyzers/indicators/candle_recognition.py b/app/analyzers/indicators/candle_recognition.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/cci.py b/app/analyzers/indicators/cci.py new file mode 100644 index 00000000..51f528dc --- /dev/null +++ b/app/analyzers/indicators/cci.py @@ -0,0 +1,41 @@ +import pandas as pd +from talib import abstract + +from analyzers.utils import IndicatorUtils + + +class CCI(IndicatorUtils): + def analyze( + self, + historical_data, + period_count=9, + signal=["cci"], + hot_thresh=-100, + cold_thresh=100, + ): + """Performs CCI analysis on the historical data + + Args: + historical_data (list): A matrix of historical OHLCV data. + period_count (int, optional): The number of data points to consider for the CCI calculation. Defaults to 9. + hot_thresh (int, optional): The threshold for identifying overbought conditions. Defaults to 100. + cold_thresh (int, optional): The threshold for identifying oversold conditions. Defaults to -100. + + Returns: + pandas.DataFrame: A dataframe containing the cci values and hot/cold signal flags. + """ + + dataframe = self.convert_to_dataframe(historical_data) + cci_values = abstract.CCI(dataframe, timeperiod=period_count).to_frame() + cci_values.dropna(how="all", inplace=True) + cci_values.rename(columns={cci_values.columns[0]: "cci"}, inplace=True) + + # Identify hot (overbought) and cold (oversold) signals + cci_values["is_hot"] = (cci_values[signal[0]] <= hot_thresh) | ( + cci_values[signal[0]] > cci_values[signal[0]].shift() + ) + cci_values["is_cold"] = (cci_values[signal[0]] >= cold_thresh) | ( + cci_values[signal[0]] < cci_values[signal[0]].shift() + ) + + return cci_values diff --git a/app/analyzers/indicators/ichimoku.py b/app/analyzers/indicators/ichimoku.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/ifish_stoch.py b/app/analyzers/indicators/ifish_stoch.py new file mode 100755 index 00000000..88c6a572 --- /dev/null +++ b/app/analyzers/indicators/ifish_stoch.py @@ -0,0 +1,40 @@ + +""" +IFISH STOCHATIC RSI indicator +""" + +import pandas as pd +import talib +import numpy as np +import pandas_ta as ta + +from analyzers.utils import IndicatorUtils + + +class IFISH_STOCH(IndicatorUtils): + def analyze(self, historical_data, signal="ifish_stoch", hot_thresh=-0.9, cold_thresh=0.9, period_count=5): + """ + Calculates the Ifish Stoch indicator using the inverse of the Fisher transform + :param df: Dataframe with close prices + :param n: Period for Stochastic oscillator + :param m: Period for the momentum indicator + :param r: Harmonic ratio for the Fisher transform + :return: Dataframe with Ifish Stoch values + """ + # Calculate Stochastic oscillator + df = pd.DataFrame() + df = self.convert_to_dataframe(historical_data) + # Calcular la transformada de Fisher estocástica + df.ta.fisher(length= period_count, signal = 9, append= True) + print(df.tail()) + + # Calcular la inversa de la transformada de Fisher estocástica + df["ifish_stoch"] = np.log((1-df["FISHERT_5_9"])/df["FISHERT_5_9"]) + print(df.tail()) + + + df["is_hot"] = (df["ifish_stoch"] < hot_thresh) & ( + df["ifish_stoch"] > df["ifish_stoch"].shift(1)) + df["is_cold"] = (df["ifish_stoch"] > cold_thresh) | ( + df["ifish_stoch"] < df["ifish_stoch"].shift(1)) + return df diff --git a/app/analyzers/indicators/iip.py b/app/analyzers/indicators/iip.py new file mode 100755 index 00000000..4fe37fb4 --- /dev/null +++ b/app/analyzers/indicators/iip.py @@ -0,0 +1,57 @@ +""" Custom Indicator Increase In Volume +""" + +import numpy as np +from scipy import stats +import pandas_ta as pta + + +from analyzers.utils import IndicatorUtils + + +class IIP(IndicatorUtils): + def analyze( + self, + historical_data, + signal=["iip"], + hot_thresh=2, + cold_thresh=0, + period_count=9, + ): + """Performs an analysis about the increase in price to detect pump dump on the historical data + + Args: + historical_data (list): A matrix of historical OHCLV data. + signal (list, optional): Defaults to iip. The indicator line to check hot against. + hot_thresh (float, optional): Defaults to 10. + cold_thresh: below hot+thresh + + + Returns: + pandas.DataFrame: A dataframe containing the indicator and hot/cold values. + """ + + dataframe = self.convert_to_dataframe(historical_data) + + dataframe["candle_length"] = np.abs( + dataframe["high"] * 100000 - dataframe["low"] * 100000 + ) + dataframe.ta.zscore( + close=dataframe["candle_length"], + length=period_count, + std=hot_thresh, + append=True, + ) + dataframe["iip"] = np.abs(dataframe[f"ZS_{period_count}"]) + dataframe.dropna(how="all", inplace=True) + + dataframe["is_hot"] = False + dataframe["is_cold"] = False + dataframe["is_hot"] = (dataframe["iip"] >= hot_thresh) & ( + dataframe["close"] > dataframe["open"] + ) + dataframe["is_cold"] = (dataframe["iip"] >= hot_thresh) & ( + dataframe["close"] < dataframe["open"] + ) + + return dataframe diff --git a/app/analyzers/indicators/iiv.py b/app/analyzers/indicators/iiv.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/klinger_oscillator.py b/app/analyzers/indicators/klinger_oscillator.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/ma_crossover.py b/app/analyzers/indicators/ma_crossover.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/ma_ribbon.py b/app/analyzers/indicators/ma_ribbon.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/macd.py b/app/analyzers/indicators/macd.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/macd_cross.py b/app/analyzers/indicators/macd_cross.py old mode 100644 new mode 100755 index b4f40e35..ae56e4f5 --- a/app/analyzers/indicators/macd_cross.py +++ b/app/analyzers/indicators/macd_cross.py @@ -29,8 +29,8 @@ def analyze(self, historical_data, signal=['macd'], hot_thresh=None, cold_thresh macd, macdsignal, macdhist = talib.MACD( dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) - macd_values = pandas.DataFrame([macd, macdsignal]).T.rename( - columns={0: "macd", 1: "signal"}) + macd_values = pandas.DataFrame([macd, macdsignal, macdhist]).T.rename( + columns={0: "macd", 1: "signal", 2: "macdhist"}) macd_cross = pandas.concat([dataframe, macd_values], axis=1) macd_cross.dropna(how='all', inplace=True) diff --git a/app/analyzers/indicators/mfi.py b/app/analyzers/indicators/mfi.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/momentum.py b/app/analyzers/indicators/momentum.py old mode 100644 new mode 100755 index 2875b5d1..f163f18d --- a/app/analyzers/indicators/momentum.py +++ b/app/analyzers/indicators/momentum.py @@ -10,33 +10,28 @@ class Momentum(IndicatorUtils): - def analyze(self, historical_data, period_count=10, - signal=['momentum'], hot_thresh=None, cold_thresh=None): - """Performs momentum analysis on the historical data - - Args: - historical_data (list): A matrix of historical OHCLV data. - period_count (int, optional): Defaults to 10. The number of data points to consider for - our momentum. - signal (list, optional): Defaults to momentum. The indicator line to check hot/cold - against. - hot_thresh (float, optional): Defaults to None. The threshold at which this might be - good to purchase. - cold_thresh (float, optional): Defaults to None. The threshold at which this might be - good to sell. - - Returns: - pandas.DataFrame: A dataframe containing the indicators and hot/cold values. - """ + def analyze( + self, + historical_data, + period_count=10, + signal=["momentum"], + hot_thresh=None, + cold_thresh=None, + ): + """Performs momentum analysis on the historical data""" dataframe = self.convert_to_dataframe(historical_data) mom_values = abstract.MOM(dataframe, period_count).to_frame() - mom_values.dropna(how='all', inplace=True) - mom_values.rename( - columns={mom_values.columns[0]: 'momentum'}, inplace=True) - - if mom_values[signal[0]].shape[0]: - mom_values['is_hot'] = mom_values[signal[0]] > hot_thresh - mom_values['is_cold'] = mom_values[signal[0]] < cold_thresh - + mom_values.dropna(how="all", inplace=True) + mom_values.rename(columns={mom_values.columns[0]: "momentum"}, inplace=True) + + if mom_values[signal[0]].shape[0] > 0: # Check if there are any momentum values + # Compare current momentum to hot threshold and to previous momentum value + mom_values["is_hot"] = (mom_values[signal[0]] < hot_thresh) & ( + mom_values[signal[0]] > mom_values[signal[0]].shift(1) + ) + + mom_values["is_cold"] = (mom_values[signal[0]] > cold_thresh) & ( + mom_values[signal[0]] < mom_values[signal[0]].shift(1) + ) return mom_values diff --git a/app/analyzers/indicators/natr.py b/app/analyzers/indicators/natr.py new file mode 100755 index 00000000..25f2c6e7 --- /dev/null +++ b/app/analyzers/indicators/natr.py @@ -0,0 +1,32 @@ +""" NATR Indicator +""" + +import math + +import pandas +from talib import abstract + +from analyzers.utils import IndicatorUtils + + +class NATR(IndicatorUtils): + def analyze(self, historical_data, signal=['natr'], hot_thresh=None, cold_thresh=None, period_count=14): + """Performs an NATR analysis on the historical data + + Args: + historical_data (list): A matrix of historical OHCLV data. + period_count (int, optional): Defaults to 14. The number of data points to consider for + our exponential moving average. + + Returns: + pandas.DataFrame: A dataframe containing the indicators and hot/cold values. + """ + + dataframe = self.convert_to_dataframe(historical_data) + natr_values = abstract.NATR(dataframe, period_count).to_frame() + natr_values.dropna(how='all', inplace=True) + natr_values.rename(columns={0: 'natr'}, inplace=True) + natr_values['is_hot'] = True + natr_values['is_cold'] = False + + return natr_values \ No newline at end of file diff --git a/app/analyzers/indicators/obv.py b/app/analyzers/indicators/obv.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/roc.py b/app/analyzers/indicators/roc.py new file mode 100755 index 00000000..aeb6afdc --- /dev/null +++ b/app/analyzers/indicators/roc.py @@ -0,0 +1,31 @@ +""" ROC Indicator +""" + +import math + +import pandas +from talib import abstract + +from analyzers.utils import IndicatorUtils + + +class ROC(IndicatorUtils): + def analyze(self, historical_data, signal=['roc'], hot_thresh=1.5, cold_thresh=-1.5, period_count=4): + """Performs an ROC analysis on the historical data + + Args: + historical_data (list): A matrix of historical OHCLV data. + period_count (int, optional): Defaults to 15. The number of data points to consider ROC + + Returns: + pandas.DataFrame: A dataframe containing the indicators and hot/cold values. + """ + + dataframe = self.convert_to_dataframe(historical_data) + roc_values = abstract.ROC(dataframe, period_count).to_frame() + roc_values.dropna(how='all', inplace=True) + roc_values.rename(columns={0: 'roc'}, inplace=True) + roc_values['is_hot'] = (roc_values["roc"] >= hot_thresh) + roc_values['is_cold'] = (roc_values["roc"] <= cold_thresh) + + return roc_values \ No newline at end of file diff --git a/app/analyzers/indicators/rsi.py b/app/analyzers/indicators/rsi.py old mode 100644 new mode 100755 diff --git a/app/analyzers/indicators/sqzmom.py b/app/analyzers/indicators/sqzmom.py old mode 100644 new mode 100755 index 53beafcd..2e9e4d26 --- a/app/analyzers/indicators/sqzmom.py +++ b/app/analyzers/indicators/sqzmom.py @@ -1,99 +1,71 @@ -""" -Squeeze Momentum Indicator - -This indicator is based on this code: -https://www.tradingview.com/script/nqQ1DT5a-Squeeze-Momentum-Indicator-LazyBear/ - -by LazyBear https://www.tradingview.com/u/LazyBear/ - -""" - import numpy as np +import pandas as pd from analyzers.utils import IndicatorUtils class SQZMOM(IndicatorUtils): - - def analyze(self, historical_data, signal=['is_hot'], hot_thresh=None, cold_thresh=None): - """Performs a macd analysis on the historical data - - Args: - historical_data (list): A matrix of historical OHCLV data. - signal (list, optional): Defaults to macd - hot_thresh (float, optional): Unused for this indicator - cold_thresh (float, optional): Unused for this indicator - - Returns: - pandas.DataFrame: A dataframe containing the indicator and hot/cold values. - """ + def analyze( + self, historical_data, signal=["is_hot"], hot_thresh=None, cold_thresh=None + ): df = self.convert_to_dataframe(historical_data) - sqzmom = df.copy(deep=True); - # parameter setup (default values in the original indicator) + # Parameters setup to match the Pine Script length = 20 - mult = 2 + mult = 2.0 length_KC = 20 mult_KC = 1.5 - # calculate Bollinger Bands - - # moving average - m_avg = df['close'].rolling(window=length).mean() - # standard deviation - m_std = df['close'].rolling(window=length).std(ddof=0) - # upper Bollinger Bands - df['upper_BB'] = m_avg + mult * m_std - # lower Bollinger Bands - df['lower_BB'] = m_avg - mult * m_std - - # calculate Keltner Channel - - # first we need to calculate True Range - df['tr0'] = abs(df["high"] - df["low"]) - df['tr1'] = abs(df["high"] - df["close"].shift()) - df['tr2'] = abs(df["low"] - df["close"].shift()) - df['tr'] = df[['tr0', 'tr1', 'tr2']].max(axis=1) - # moving average of the TR - range_ma = df['tr'].rolling(window=length_KC).mean() - # upper Keltner Channel - df['upper_KC'] = m_avg + range_ma * mult_KC - # lower Keltner Channel - df['lower_KC'] = m_avg - range_ma * mult_KC - - # check for 'squeeze' - df['squeeze_on'] = (df['lower_BB'] > df['lower_KC']) & (df['upper_BB'] < df['upper_KC']) - df['squeeze_off'] = (df['lower_BB'] < df['lower_KC']) & (df['upper_BB'] > df['upper_KC']) - - # calculate momentum value - highest = df['high'].rolling(window = length_KC).max() - lowest = df['low'].rolling(window = length_KC).min() + # Calculate Bollinger Bands (BB) + basis = df["close"].rolling(window=length).mean() + dev = mult * df["close"].rolling(window=length).std(ddof=0) + df["upper_BB"] = basis + dev + df["lower_BB"] = basis - dev + + # Calculate Keltner Channel (KC) + tr = pd.DataFrame( + { + "tr0": abs(df["high"] - df["low"]), + "tr1": abs(df["high"] - df["close"].shift()), + "tr2": abs(df["low"] - df["close"].shift()), + } + ).max(axis=1) + rangema = tr.rolling(window=length_KC).mean() + ma = df["close"].rolling(window=length_KC).mean() + df["upper_KC"] = ma + rangema * mult_KC + df["lower_KC"] = ma - rangema * mult_KC + + # Squeeze detection + df["sqzOn"] = (df["lower_BB"] > df["lower_KC"]) & ( + df["upper_BB"] < df["upper_KC"] + ) + df["sqzOff"] = (df["lower_BB"] < df["lower_KC"]) & ( + df["upper_BB"] > df["upper_KC"] + ) + df["noSqz"] = ~(df["sqzOn"] | df["sqzOff"]) + + # Momentum calculation (val) + highest = df["high"].rolling(window=length_KC).max() + lowest = df["low"].rolling(window=length_KC).min() m1 = (highest + lowest) / 2 - df['value'] = (df['close'] - (m1 + m_avg)/2) - fit_y = np.array(range(0,length_KC)) - df['value'] = df['value'].rolling(window = length_KC).apply(lambda x : np.polyfit(fit_y, x, 1)[0] * (length_KC-1) + - np.polyfit(fit_y, x, 1)[1], raw=True) - - - # entry point for long position: - # 1. black cross becomes gray (the squeeze is released) - long_cond1 = (df['squeeze_off'][-2] == False) & (df['squeeze_off'][-1] == True) - # 2. bar value is positive => the bar is light green - long_cond2 = df['value'][-1] > 0 - enter_long = long_cond1 and long_cond2 - - # entry point for short position: - # 1. black cross becomes gray (the squeeze is released) - short_cond1 = (df['squeeze_off'][-2] == False) & (df['squeeze_off'][-1] == True) - # 2. bar value is negative => the bar is light red - short_cond2 = df['value'][-1] < 0 - enter_short = short_cond1 and short_cond2 - - sqzmom['is_hot'] = False - sqzmom['is_cold'] = False - - sqzmom.at[sqzmom.index[-1], 'is_hot'] = enter_long - sqzmom.at[sqzmom.index[-1], 'is_cold'] = enter_short - - return sqzmom + close_avg = df["close"].rolling(window=length_KC).mean() + # Adjusted source for linear regression to match Pine Script's val + source_adj = df["close"] - ((m1 + close_avg) / 2) + val = source_adj.rolling(window=length_KC).apply( + lambda x: np.polyfit(np.arange(len(x)), x, 1)[0] * (length_KC - 1) + + np.polyfit(np.arange(len(x)), x, 1)[1], + raw=True, + ) + + # Conditions for entering positions + # Long condition (squeeze turns off, not in no squeeze condition, and val is positive) + df["long_cond"] = df["sqzOff"] & ~df["noSqz"] & (val > 0) + # Short condition (squeeze turns off, not in no squeeze condition, and val is negative) + df["short_cond"] = df["sqzOff"] & ~df["noSqz"] & (val < 0) + + # Assign long and short signals to the dataframe + df["is_hot"] = df["long_cond"] + df["is_cold"] = df["short_cond"] + + return df diff --git a/app/analyzers/indicators/stoch_hma.py b/app/analyzers/indicators/stoch_hma.py new file mode 100755 index 00000000..7e1caa34 --- /dev/null +++ b/app/analyzers/indicators/stoch_hma.py @@ -0,0 +1,54 @@ +import pandas as pd +import numpy as np + +from analyzers.informants.lrsi import LRSI +from analyzers.indicators.stoch_rsi import StochasticRSI +from analyzers.utils import IndicatorUtils + +def weighted_moving_average(dataframe, period): + weights = np.arange(1, period + 1) + wma = dataframe['close'].rolling(window=period).apply(lambda x: np.dot(x, weights) / weights.sum(), raw=True) + return wma + +def hull_moving_average(dataframe, period=9): + half_length = int(period / 2) + sqrt_length = int(np.sqrt(period)) + wmaf = weighted_moving_average(dataframe, half_length) + wmas = weighted_moving_average(dataframe, sqrt_length) + hma = weighted_moving_average((2 * wmaf - wmas).to_frame(name='close'), sqrt_length) + return hma + +def stochastic_hma(dataframe, period=9): + hma = hull_moving_average(dataframe, period) + lowest_hma = hma.rolling(window=period).min() + highest_hma = hma.rolling(window=period).max() + stoch_hma = 100 * (hma - lowest_hma) / (highest_hma - lowest_hma) + return stoch_hma + +class StochHMA(IndicatorUtils): + def analyze(self, historical_data, signal='stoch_hma', period_count=9, hot_thresh=80, cold_thresh=80): + dataframe = self.convert_to_dataframe(historical_data) + stoch_hma_values = stochastic_hma(dataframe, period_count).to_frame(name='stoch_hma') + stoch_hma_values.dropna(how='all', inplace=True) + + # Calculate the previous value of 'stoch_hma' and store in the same DataFrame + stoch_hma_values['prev_stoch_hma'] = stoch_hma_values['stoch_hma'].shift() + + # Determine hot and cold signals using 'apply' with lambda function + stoch_hma_values['is_hot'] = stoch_hma_values.apply( + lambda row: ((row['stoch_hma'] <= hot_thresh) & (row['stoch_hma'] > row['prev_stoch_hma'])) | (row['stoch_hma'] == 0), + axis=1 + ) + + stoch_hma_values['is_cold'] = stoch_hma_values.apply( + lambda row: (row['stoch_hma'] > cold_thresh) | (row['stoch_hma'] < row['prev_stoch_hma']) | (row['stoch_hma'] == 100), + axis=1 + ) + + # Drop the 'prev_stoch_hma' column if not needed further + stoch_hma_values.drop('prev_stoch_hma', axis=1, inplace=True) + + return stoch_hma_values + + + diff --git a/app/analyzers/indicators/stoch_hma_avg_rsi.py b/app/analyzers/indicators/stoch_hma_avg_rsi.py new file mode 100755 index 00000000..ba050668 --- /dev/null +++ b/app/analyzers/indicators/stoch_hma_avg_rsi.py @@ -0,0 +1,112 @@ +""" +HMA of Stock RSI plus Laguerre RSI Indicator +""" + +import pandas as pd +import numpy as np + +from analyzers.informants.lrsi import LRSI +from analyzers.indicators.stoch_rsi import StochasticRSI + +from analyzers.utils import IndicatorUtils + + +def weighted_moving_average(dataframe, period): + """ + Calculate Weighted Moving Average (WMA) + + Args: + dataframe (pd.DataFrame): Input data frame containing the 'close' column. + period (int): The period over which to calculate WMA. + + Returns: + pd.Series: A series containing the WMA. + """ + weights = np.arange(1, period + 1) # Weighting factors + wma = dataframe['close'].rolling(window=period).apply(lambda x: np.dot(x, weights) / weights.sum(), raw=True) + return wma + +def hull_moving_average(dataframe, period=3): + """ + Calculate Hull Moving Average (HMA) + + Args: + dataframe (pd.DataFrame): Input data frame containing the 'close' column. + period (int): The period over which to calculate HMA. + + Returns: + pd.Series: A series containing the HMA. + """ + half_length = int(period / 2) + sqrt_length = int(np.sqrt(period)) + + # First WMA for half length + wmaf = weighted_moving_average(dataframe, half_length) + + # Second WMA for full length + wmas = weighted_moving_average(dataframe, sqrt_length) + + # Calculate the HMA: WMA of (2 * WMA_half - WMA_full) over sqrt_length + hma = weighted_moving_average((2 * wmaf - wmas).to_frame(name='close'), sqrt_length) + + return hma + +class StochHMAAVGRSI(IndicatorUtils): + + def analyze(self, historical_data, signal='stoch_hma_avg_rsi', period_count=9, hot_thresh=50, cold_thresh=50): + """ + Performs an analysis by combining Hull Moving Average (HMA) of the Stock RSI with the Laguerre RSI (LRSI) to generate trading signals. + + Args: + historical_data (list): A matrix of historical OHCLV data. + period_count (int, optional): Defaults to 9. The number of data points to consider for our HMA. + hot_thresh (float, optional): Defaults to 80. The threshold at which this might be good to purchase. + cold_thresh (float, optional): Defaults to 100. The threshold at which this might be good to sell. + + Returns: + pd.DataFrame: A dataframe containing the indicators and hot/cold values. + """ + + # Convert historical data to a pandas DataFrame + dataframe = self.convert_to_dataframe(historical_data) + + # Initialize and calculate LRSI and StochasticRSI + lrsi_df = LRSI().analyze(historical_data, period_count) + stoch_rsi_df = StochasticRSI().analyze(historical_data, period_count) + + # Drop NaN values + lrsi_df.dropna(how='all', inplace=True) + stoch_rsi_df.dropna(how='all', inplace=True) + + # Merge the dataframes + dataframe = pd.merge(lrsi_df, stoch_rsi_df, left_index=True, right_index=True) + + # Normalize LRSI + dataframe['lrsi_norm'] = dataframe['lrsi'] * 100 + + # Calculate average of LRSI normalized and slow_k, slow_d from StochasticRSI + avg_rsi = pd.DataFrame() + avg_rsi['close'] = (dataframe['lrsi_norm'] + dataframe['slow_k'] + dataframe['slow_d']) / 3 + + stoch_hma_avg_values = hull_moving_average(avg_rsi) + stoch_hma_avg_values.dropna(inplace=True) + stoch_hma_avg_values = stoch_hma_avg_values.to_frame(name='stoch_hma_avg_rsi') + + stoch_hma_avg_values['prev_stoch_hma'] = stoch_hma_avg_values['stoch_hma_avg_rsi'].shift() + + # Determine hot and cold signals + stoch_hma_avg_values['is_hot'] = stoch_hma_avg_values['stoch_hma_avg_rsi'].apply(lambda x: x <= hot_thresh) + stoch_hma_avg_values['is_cold'] = stoch_hma_avg_values['stoch_hma_avg_rsi'].apply(lambda x: x > cold_thresh) + + # Determine hot and cold signals using 'apply' with lambda function + stoch_hma_avg_values['is_hot'] = stoch_hma_avg_values.apply( + lambda row: ((row['stoch_hma_avg_rsi'] <= hot_thresh) & (row['stoch_hma_avg_rsi'] > row['prev_stoch_hma'])) | (row['stoch_hma_avg_rsi'] == 0), + axis=1 + ) + + stoch_hma_avg_values['is_cold'] = stoch_hma_avg_values.apply( + lambda row: (row['stoch_hma_avg_rsi'] > cold_thresh) | (row['stoch_hma_avg_rsi'] < row['prev_stoch_hma']) | (row['stoch_hma_avg_rsi'] == 100), + axis=1 + ) + + return stoch_hma_avg_values diff --git a/app/analyzers/indicators/stoch_rsi.py b/app/analyzers/indicators/stoch_rsi.py old mode 100644 new mode 100755 index 6ce94167..9c370c0f --- a/app/analyzers/indicators/stoch_rsi.py +++ b/app/analyzers/indicators/stoch_rsi.py @@ -6,12 +6,14 @@ import numpy import pandas from talib import abstract +import pandas_ta as pta + from analyzers.utils import IndicatorUtils class StochasticRSI(IndicatorUtils): - def analyze(self, historical_data, period_count=14, + def analyze(self, historical_data, period_count=9, signal=['stoch_rsi'], hot_thresh=None, cold_thresh=None): """Performs a Stochastic RSI analysis on the historical data @@ -32,20 +34,15 @@ def analyze(self, historical_data, period_count=14, dataframe = self.convert_to_dataframe(historical_data) - rsi = abstract.RSI(dataframe, period_count) - - stochrsi = (rsi - rsi.rolling(period_count).min()) / (rsi.rolling(period_count).max() - rsi.rolling(period_count).min()) - stochrsi_K = stochrsi.rolling(3).mean() - stochrsi_D = stochrsi_K.rolling(3).mean() - - kd_values = pandas.DataFrame([stochrsi, stochrsi_K, stochrsi_D]).T.rename( - columns={0: "stoch_rsi", 1: "slow_k", 2: "slow_d"}).copy() + df = pandas.DataFrame() + df = dataframe.copy() + df.ta.stochrsi(length=9, rsi_length= 9, k=3, d=3,append=True) - kd_values['stoch_rsi'] = kd_values['stoch_rsi'].multiply(100) - kd_values['slow_k'] = kd_values['slow_k'].multiply(100) - kd_values['slow_d'] = kd_values['slow_d'].multiply(100) + df['stoch_rsi'] = df['STOCHRSIk_9_9_3_3'] + df['slow_k'] = df['STOCHRSIk_9_9_3_3'] + df['slow_d'] = df['STOCHRSId_9_9_3_3'] - stoch_rsi = pandas.concat([dataframe, kd_values], axis=1) + stoch_rsi = pandas.concat([dataframe, df], axis=1) stoch_rsi.dropna(how='all', inplace=True) if stoch_rsi[signal[0]].shape[0]: diff --git a/app/analyzers/indicators/stochrsi_cross.py b/app/analyzers/indicators/stochrsi_cross.py old mode 100644 new mode 100755 index 8881dcc8..7600446d --- a/app/analyzers/indicators/stochrsi_cross.py +++ b/app/analyzers/indicators/stochrsi_cross.py @@ -10,7 +10,7 @@ class StochRSICross(IndicatorUtils): - def analyze(self, historical_data, period_count=14, signal=['stoch_rsi'], smooth_k = 10, smooth_d = 3, hot_thresh=None, cold_thresh=None): + def analyze(self, historical_data, period_count=9, signal=['stoch_rsi'], smooth_k = 3, smooth_d = 3, hot_thresh=None, cold_thresh=None): """Performs a StochRSI cross analysis on the historical data Args: @@ -27,28 +27,23 @@ def analyze(self, historical_data, period_count=14, signal=['stoch_rsi'], smooth dataframe = self.convert_to_dataframe(historical_data) - rsi = abstract.RSI(dataframe, period_count) - stochrsi = (rsi - rsi.rolling(period_count).min()) / (rsi.rolling(period_count).max() - rsi.rolling(period_count).min()) - stochrsi_K = stochrsi.rolling(smooth_k).mean() - stochrsi_D = stochrsi_K.rolling(smooth_d).mean() + df = pandas.DataFrame() + df = dataframe.copy() + df.ta.stochrsi(length=9, rsi_length=9, k=3, d=3, append=True) - kd_values = pandas.DataFrame([rsi, stochrsi, stochrsi_K, stochrsi_D]).T.rename( - columns={0: "rsi", 1: "stoch_rsi", 2: "smooth_k", 3: "smooth_d"}).copy() + df['stoch_rsi'] = df['STOCHRSIk_9_9_3_3'] + df['smooth_k'] = df['STOCHRSIk_9_9_3_3'] + df['smooth_d'] = df['STOCHRSId_9_9_3_3'] - kd_values['stoch_rsi'] = kd_values['stoch_rsi'].multiply(100) - kd_values['smooth_k'] = kd_values['smooth_k'].multiply(100) - kd_values['smooth_d'] = kd_values['smooth_d'].multiply(100) + df.dropna(how='all', inplace=True) + + previous_k, previous_d = df.iloc[-2]['smooth_k'], df.iloc[-2]['smooth_d'] + current_k, current_d = df.iloc[-1]['smooth_k'], df.iloc[-1]['smooth_d'] - stoch_cross = pandas.concat([dataframe, kd_values], axis=1) - stoch_cross.dropna(how='all', inplace=True) + df['is_hot'] = False + df['is_cold'] = False - previous_k, previous_d = stoch_cross.iloc[-2]['smooth_k'], stoch_cross.iloc[-2]['smooth_d'] - current_k, current_d = stoch_cross.iloc[-1]['smooth_k'], stoch_cross.iloc[-1]['smooth_d'] + df.at[df.index[-1], 'is_cold'] = previous_k > previous_d and current_k < current_d + df.at[df.index[-1], 'is_hot'] = previous_k < previous_d and current_k > current_d - stoch_cross['is_hot'] = False - stoch_cross['is_cold'] = False - - stoch_cross.at[stoch_cross.index[-1], 'is_cold'] = previous_k > previous_d and current_k < current_d - stoch_cross.at[stoch_cross.index[-1], 'is_hot'] = previous_k < previous_d and current_k > current_d - - return stoch_cross + return df diff --git a/app/analyzers/informants/__init__.py b/app/analyzers/informants/__init__.py old mode 100644 new mode 100755 index ea9cf0fe..e3f21598 --- a/app/analyzers/informants/__init__.py +++ b/app/analyzers/informants/__init__.py @@ -1,8 +1,8 @@ __all__ = [ - 'bollinger_bands', 'ema', 'sma', 'vwap', 'ohlcv', - 'lrsi' + 'lrsi', + 'hma', ] diff --git a/app/analyzers/informants/ema.py b/app/analyzers/informants/ema.py old mode 100644 new mode 100755 diff --git a/app/analyzers/informants/hma.py b/app/analyzers/informants/hma.py new file mode 100755 index 00000000..5a778453 --- /dev/null +++ b/app/analyzers/informants/hma.py @@ -0,0 +1,50 @@ +""" HMA Indicator +""" + +import math +import pandas as pd +import numpy as np +import pandas +from talib import abstract + +from analyzers.utils import IndicatorUtils + +def hull_moving_average(dataframe, period=9): + """ + Calculate Hull Moving Average (HMA) + + Args: + dataframe (pd.DataFrame): Input data frame containing the 'close' column. + period (int): The period over which to calculate HMA. + + Returns: + pd.Series: A series containing the HMA. + """ + half_length = int(period / 2) + sqrt_length = int(np.sqrt(period)) + wmaf = dataframe['close'].rolling(window=half_length).mean() + wmas = dataframe['close'].rolling(window=period).mean() + dataframe['hma'] = 2 * wmaf - wmas + hma = dataframe['hma'].rolling(window=sqrt_length).mean() + + return hma + +class HMA(IndicatorUtils): + def analyze(self, historical_data, period_count=3): + """Performs an EMA analysis on the historical data + + Args: + historical_data (list): A matrix of historical OHCLV data. + period_count (int, optional): Defaults to 15. The number of data points to consider for + our exponential moving average. + + Returns: + pandas.DataFrame: A dataframe containing the indicators and hot/cold values. + """ + + dataframe = self.convert_to_dataframe(historical_data) + hma_values = hull_moving_average(dataframe, period_count).to_frame() + hma_values.dropna(how='all', inplace=True) + hma_values.rename(columns={0: 'hma'}, inplace=True) + + return hma_values diff --git a/app/analyzers/informants/lrsi.py b/app/analyzers/informants/lrsi.py old mode 100644 new mode 100755 index fb761354..9902047d --- a/app/analyzers/informants/lrsi.py +++ b/app/analyzers/informants/lrsi.py @@ -10,7 +10,7 @@ This provides for faster reactions to price changes ``gamma`` is meant to have values between ``0.2`` and ``0.8``, with the - best balance found theoretically at the default of ``0.5`` + best balance found theoretically at the default of ``0.7`` """ import numpy as np @@ -27,10 +27,10 @@ def apply_filter(self, price, gamma): l2_1 = self.l2 g = gamma # avoid more lookups - self.l0 = l0 = (1.0 - g) * price + g * l0_1 - self.l1 = l1 = -g * l0 + l0_1 + g * l1_1 - self.l2 = l2 = -g * l1 + l1_1 + g * l2_1 - self.l3 = l3 = -g * l2 + l2_1 + g * self.l3 + self.l0 = l0 = g * price + (1 - g) * l0_1 + self.l1 = l1 = -(1-g) * l0 + l0_1 + (1-g) * l1_1 + self.l2 = l2 = -(1-g) * l1 + l1_1 + (1-g) * l2_1 + self.l3 = l3 = -(1-g) * l2 + l2_1 + (1-g) * self.l3 cu = 0.0 cd = 0.0 @@ -49,16 +49,19 @@ def apply_filter(self, price, gamma): else: cd += l3 - l2 - den = cu + cd + if cu + cd != 0.00: + den = cu / (cu + cd) + else: + den = 0.00 - return 1.0 if not den else cu / den + return den def analyze(self, historical_data, signal=['lrsi']): """Performs a better implementation of RSI Args: historical_data (list): A matrix of historical OHCLV data. - signal (list, optional): Defaults to iiv. The indicator line to check hot against. + signal (list, optional): Defaults to lrsi. The indicator line to check hot against. hot_thresh (float, optional): Defaults to 0.2. The threshold at which this might be good to purchase. cold_thresh: Defaults to 0.8. The threshold at which this might be @@ -72,6 +75,6 @@ def analyze(self, historical_data, signal=['lrsi']): dataframe = self.convert_to_dataframe(historical_data) dataframe['lrsi'] = dataframe.close.apply( - lambda x: self.apply_filter(x, 0.4)) + lambda x: self.apply_filter(x, 0.5)) - return dataframe + return dataframe \ No newline at end of file diff --git a/app/analyzers/informants/ohlcv.py b/app/analyzers/informants/ohlcv.py old mode 100644 new mode 100755 diff --git a/app/analyzers/informants/sma.py b/app/analyzers/informants/sma.py old mode 100644 new mode 100755 diff --git a/app/analyzers/informants/vwap.py b/app/analyzers/informants/vwap.py old mode 100644 new mode 100755 diff --git a/app/analyzers/uptrend.py b/app/analyzers/uptrend.py new file mode 100755 index 00000000..64aca043 --- /dev/null +++ b/app/analyzers/uptrend.py @@ -0,0 +1,46 @@ +""" UpTrend analysis indicator +""" + +import numpy +import pandas +from talib import abstract + +from analyzers.utils import IndicatorUtils + + +class UpTrend(IndicatorUtils): + def analyze(self, key_indicator, key_indicator_index, key_signal, key_period_count=1): + """ Tests for key_indicator is going uptrend seeing period count back. + + Args: + key_indicator (pandas.DataFrame): A dataframe containing the results of the analysiscrossover.py + for the selected key indicator. + key_signal (str): The name of the key indicator. + key_indicator_index (int): The configuration index of the key indicator to use. + key_period_count (integer): how many period analyzer needs to go back to compare current period. Default 1 + + Returns: + pandas.DataFrame: A dataframe containing the indicators and hot/cold values. + """ + + key_indicator_name = '{}_{}'.format(key_signal, key_indicator_index) + new_key_indicator = key_indicator.copy(deep=True) + for column in new_key_indicator: + column_indexed_name = '{}_{}'.format(column, key_indicator_index) + new_key_indicator.rename(columns={column: column_indexed_name}, inplace=True) + + uptrended_indicator_name = '{}_{}_uptrended'.format(key_signal, key_indicator_index) + uptrended_indicator_index = key_indicator_index + new_uptrended_indicator = key_indicator.copy(deep=True) + new_uptrended_indicator = new_uptrended_indicator.shift(periods=key_period_count) + for column in new_uptrended_indicator: + column_indexed_name = '{}_{}_uptrended'.format(column, uptrended_indicator_index) + new_uptrended_indicator.rename(columns={column: column_indexed_name}, inplace=True) + + combined_data = pandas.concat([new_key_indicator, new_uptrended_indicator], axis=1) + combined_data.dropna(how='any', inplace=True) + + combined_data['is_hot'] = combined_data[key_indicator_name] > combined_data[uptrended_indicator_name] + combined_data['is_cold'] = combined_data[key_indicator_name] < combined_data[uptrended_indicator_name] + + return combined_data diff --git a/app/analyzers/utils.py b/app/analyzers/utils.py old mode 100644 new mode 100755 diff --git a/app/app.py b/app/app.py old mode 100644 new mode 100755 diff --git a/app/behaviour.py b/app/behaviour.py old mode 100644 new mode 100755 index f0f64658..54b4d853 --- a/app/behaviour.py +++ b/app/behaviour.py @@ -37,6 +37,7 @@ def __init__(self, config, exchange_interface, notifier): self.indicator_conf = config.indicators self.informant_conf = config.informants self.crossover_conf = config.crossovers + self.uptrend_conf = config.uptrends self.exchange_interface = exchange_interface self.strategy_analyzer = StrategyAnalyzer() self.notifier = notifier @@ -164,6 +165,10 @@ def _test_strategies(self, market_data, output_mode): new_result[exchange][market_pair] ) + new_result[exchange][market_pair]['uptrends'] = self._get_uptrend_results( + new_result[exchange][market_pair] + ) + if output_mode in self.output: output_data = deepcopy(new_result[exchange][market_pair]) print( @@ -373,6 +378,52 @@ def _get_crossover_results(self, new_result): }) return results + def _get_uptrend_results(self, new_result): + """Execute uptrend analysis on the results so far. + + Args: + new_result (dict): A dictionary containing the results of the informant and indicator + analysis. + + Returns: + list: A list of dictionaries containing the results of the analysis. + """ + + uptrend_dispatcher = self.strategy_analyzer.uptrend_dispatcher() + results = {uptrend: list() + for uptrend in self.uptrend_conf.keys()} + for uptrend in self.uptrend_conf: + if uptrend not in uptrend_dispatcher: + self.logger.warn("No such uptrend %s, skipping.", uptrend) + continue + + for uptrend_conf in self.uptrend_conf[uptrend]: + if not uptrend_conf['enabled']: + self.logger.debug("%s is disabled, skipping.", uptrend) + continue + try: + key_indicator = new_result[uptrend_conf['key_indicator_type']][uptrend_conf['key_indicator']][uptrend_conf['key_indicator_index']] + + uptrend_conf['candle_period'] = uptrend_conf['key_indicator'] + \ + str(uptrend_conf['key_indicator_index']) + + dispatcher_args = { + 'key_indicator': key_indicator['result'], + 'key_indicator_index': uptrend_conf['key_indicator_index'], + 'key_signal': uptrend_conf['key_signal'], + 'key_period_count': uptrend_conf['key_period_count'] + } + except Exception as e: + self.logger.warning(e) + self.logger.warning(traceback.format_exc()) + continue + + results[uptrend].append({ + 'result': uptrend_dispatcher[uptrend](**dispatcher_args), + 'config': uptrend_conf + }) + return results + def _get_historical_data(self, market_pair, exchange, candle_period): """Gets a list of OHLCV data for the given pair and exchange. diff --git a/app/conf.py b/app/conf.py old mode 100644 new mode 100755 index 788e3c1c..a4ad15e4 --- a/app/conf.py +++ b/app/conf.py @@ -54,6 +54,12 @@ def __init__(self): else: self.crossovers = default_config['crossovers'] + if 'uptrends' in user_config: + self.uptrends = { + **default_config['uptrends'], **user_config['uptrends']} + else: + self.uptrends = default_config['uptrends'] + if 'exchanges' in user_config: self.exchanges = user_config['exchanges'] else: diff --git a/app/config copy.bk2 b/app/config copy.bk2 new file mode 100755 index 00000000..f12c545b --- /dev/null +++ b/app/config copy.bk2 @@ -0,0 +1,653 @@ +# COPIED AND MODIFIED FROM APP/CONFIG.YML +# This is the config.yml to modify + +settings: + log_mode: text + log_level: INFO + enable_charts: false + output_mode: cli + update_interval: 300 + start_worker_interval: 30 + market_data_chunk_size: 50 + # Default timezome UTC + timezone: America/Caracas + market_pairs: + # - BNB/USDT + # - AXS/USDT + # - AAVE/USDT + # - ADA/USDT + # - ALGO/USDT + # - APT/USDT + # - ATOM/USDT + # - AVAX/USDT + - BTC/USDT + # - CAKE/USDT + # - DOGE/USDT + # - DOT/USDT + # - ETC/USDT + # - ETH/USDT + # - FET/USDT + # - FTT/USDT + # - GALA/USDT + # - GMT/USDT + # - LINK/USDT + # - LOKA/USDT + # - LTC/USDT + # - MANA/USDT + # - MATIC/USDT + # - NEAR/USDT + # - OCEAN/USDT + # - RUNE/USDT + # - SAND/USDT + # - SHIB/USDT + # - SOL/USDT + # - SUSHI/USDT + # - TRX/USDT + # - UNI/USDT + # - XRP/USDT + # - OCEAN/USDT + # - TWT/USDT + + +exchanges: + binance: + required: + enabled: true + # all_pairs: + # - USDT + # exclude: + # - USDC + # - PAX + # - USDT + +notifiers: + stdout: + required: + enable: true + optional: + template: " {{base_currency}} {{ decimal_format|format(price_value['15m'].close) }} {{status}} {{values}}" + + telegram: + required: + enable: true + token: 1737113137:AAFuVKpyYN_Nle0ffnYPNKSXcgg1CnTWROY + chat_id: -834013906 + optional: + parse_mode: markdown + template: " {% if base_currency in ['BTC','BNB','SOL','AXS','ETH'] %} + *${{base_currency}}* *{{ decimal_format|format(price_value['15m'].close) }}* *{{status}}* + {% elif status not in ['DownTREND-1h','UpTREND-1h','DownTREND-4h', 'UpTREND-4h','PUMP','DUMP' ] %} + *{{base_currency}}* *{{ decimal_format|format(price_value['15m'].close) }}* *{{status}}* + {% endif %}" + +indicators: + rsi: + - enabled: false + + macd: + - enabled: false + + stoch_rsi: + - enabled: true + candle_period: 15m + alert_enabled: true + alert_frequency: always + cold: 60 + hot: 60 + signal: + - stoch_rsi + # - slow_k + # - slow_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 15m' + mute_cold: false + - enabled: true + candle_period: 1h + alert_enabled: true + alert_frequency: always + cold: 60 + hot: 60 + signal: + - stoch_rsi + - slow_k + # - slow_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 1h' + mute_cold: false + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: always + cold: 70 + hot: 70 + signal: + - stoch_rsi + - slow_k + # - slow_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 4h' + mute_cold: false + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: always + cold: 80 + hot: 80 + signal: + - stoch_rsi + - slow_k + # - slow_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 4h' + mute_cold: true + + stochrsi_cross: + - enabled: true + candle_period: 1h + alert_enabled: true + alert_frequency: once + cold: 80 + hot: 80 + signal: + # - stoch_rsi + - smooth_k + - smooth_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 1h' + mute_cold: false + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: once + cold: 70 + hot: 70 + signal: + # - stoch_rsi + - smooth_k + - smooth_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 4h' + mute_cold: false + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: once + cold: 70 + hot: 70 + signal: + # - stoch_rsi + - smooth_k + - smooth_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 4h' + mute_cold: false + + iip: + - enabled: true + alert_enabled: true + alert_frequency: always + signal: + - iip + hot: 1.6 + candle_period: 15m + period_count: 14 + mute_cold: false + + ma_crossover: + - enabled: false + + macd_cross: + - enabled: true + alert_enabled: true + candle_period: 15m + alert_frequency: always + signal: + - macd + - signal + - macdhist + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'MACD Cross 15m' + - enabled: true + alert_enabled: true + candle_period: 1h + alert_frequency: always + signal: + - macd + - signal + - macdhist + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'MACD Cross 50m' + mute_cold: false + - enabled: true + alert_enabled: true + candle_period: 4h + alert_frequency: always + signal: + - macd + - signal + - macdhist + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'MACD Cross 4h' + + + ichimoku: + - enabled: false + momentum: + - enabled: false + cci: + - enabled: false + mfi: + - enabled: false + obv: + - enabled: false + bollinger: + - enabled: false + + bbp: + - enabled: true + candle_period: 15m + alert_enabled: true + alert_frequency: always + period_count: 20 + hot: 0.33 + cold: 0.4 + std_dev: 2 + signal: + - bbp + # - mfi + hot_label: 'Lower Band' + cold_label: 'Upper Band' + indicator_label: 'Bollinger Crossing 15m' + mute_cold: false + - enabled: true + candle_period: 1h + alert_enabled: true + alert_frequency: always + period_count: 20 + hot: 0.5 + cold: 0.5 + std_dev: 2 + signal: + - bbp + # - mfi + hot_label: 'Lower Band' + cold_label: 'Upper Band' + indicator_label: 'Bollinger Crossing 1h' + mute_cold: false + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: always + period_count: 20 + hot: 0.5 + cold: 0.5 + std_dev: 2 + signal: + - bbp + # - mfi + hot_label: 'Lower Band' + cold_label: 'Upper Band' + indicator_label: 'Bollinger Crossing 4h' + mute_cold: false + natr: + - enabled: true + alert_enabled: true + alert_frequency: always + signal: + - natr + candle_period: 15m + period_count: 9 + hot_label: 'Hot' + cold_label: 'Cold' + indicator_label: 'NATR' + mute_cold: false + bollinger_bands: + - enabled: true + alert_enabled: true + alert_frequency: always + signal: + # - upperband + # - middleband + # - lowerband + - bbwidth + candle_period: 15m + period_count: 20 + hot_label: 'Hot' + cold_label: 'Cold' + indicator_label: 'BB' + mute_cold: false + ifish_stoch: + - enabled: false + alert_enabled: true + alert_frequency: always + signal: + - ifish_stoch + candle_period: 15m + period_count: 5 + hot: -0.9 + cold: 0.9 + hot_label: 'Hot' + cold_label: 'Cold' + indicator_label: 'Ifish Stoch' + mute_cold: false + + + roc: + - enabled: true + alert_enabled: true + alert_frequency: always + alert_enabled: true + signal: + - roc + candle_period: 15m + period_count: 12 + hot_label: 'Hot' + cold_label: 'Cold' + indicator_label: 'ROC' + mute_cold: false + + + +informants: + lrsi: + - enabled: false + alert_enabled: true + signal: + - lrsi + candle_period: 15m + + vwap: + - enabled: false + sma: + - enabled: false + ema: + - enabled: false + + ohlcv: + - enabled: true + signal: + # - open + # - high + # - low + - close + # - volume + candle_period: 15m + + +crossovers: + std_crossover: + - enabled: false + +uptrends: + std_uptrend: + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 0 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: macd_cross + key_indicator_index: 0 + key_indicator_type: indicators + key_signal: macdhist + key_period_count: 1 + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 1 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: macd_cross + key_indicator_index: 1 + key_indicator_type: indicators + key_signal: macdhist + key_period_count: 1 + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 2 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: macd_cross + key_indicator_index: 2 + key_indicator_type: indicators + key_signal: macdhist + key_period_count: 1 + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 1 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 2 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 3 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + +conditionals: +# SIGNALS + +#### TRENDS + - label: "DownTREND-1h" + cold: + - stochrsi_cross: 0 + - stoch_rsi: 1 + - label: "DownTREND-4h" + cold: + - stochrsi_cross: 1 + - stoch_rsi: 2 + - label: "UpTREND-1h" + hot: + - stochrsi_cross: 0 + - stoch_rsi: 1 + - ifish_stoch: 1 + - label: "UpTREND-4h" + hot: + - stochrsi_cross: 1 + - stoch_rsi: 2 + # - ifish_stoch: 1 + - label: "UpTREND-4h" + hot: + - stochrsi_cross: 2 + - stoch_rsi: 3 + +### PUMP DUMP + + - label: "PUMP" + hot: + - iip: 0 + + - label: "DUMP" + cold: + - iip: 0 + + +###### + - label: "UpSTRONG PUMP" + hot: + - bbp: 0 + - stoch_rsi: 0 + - std_uptrend: 0 + # - std_uptrend: 1 + - iip: 0 + - bbp: 1 + - stoch_rsi: 1 + - std_uptrend: 2 + # - std_uptrend: 3 + - bbp: 2 + - stoch_rsi: 2 + - std_uptrend: 4 + # - std_uptrend: 5 + - natr: 0 + - roc: 0 + - bollinger_bands: 0 + - stoch_rsi: 3 + # - std_uptrend: 8 + + + + - label: "UpSTRONG" + hot: + - bbp: 0 + - stoch_rsi: 0 + - std_uptrend: 0 + # - std_uptrend: 1 + - bbp: 1 + - stoch_rsi: 1 + - std_uptrend: 2 + # - std_uptrend: 3 + - bbp: 2 + - stoch_rsi: 2 + - std_uptrend: 4 + # - std_uptrend: 5 + - natr: 0 + - roc: 0 + - bollinger_bands: 0 + - stoch_rsi: 3 + # - std_uptrend: 8 + + - label: "UpMEDIUM" + hot: + - bbp: 0 + - stoch_rsi: 0 + - std_uptrend: 0 + # - std_uptrend: 1 + - bbp: 1 + - stoch_rsi: 1 + - std_uptrend: 2 + # - std_uptrend: 3 + - std_uptrend: 4 + # - std_uptrend: 5 + - natr: 0 + - roc: 0 + - bollinger_bands: 0 + - stoch_rsi: 3 + # - std_uptrend: 8 + cold: + - bbp: 2 + + - label: "UpWEAK" + hot: + - bbp: 0 + - stoch_rsi: 0 + - std_uptrend: 0 + # - std_uptrend: 1 + - stoch_rsi: 1 + # - std_uptrend: 2 + # - std_uptrend: 3 + - std_uptrend: 4 + # - std_uptrend: 5 + - natr: 0 + - roc: 0 + - bollinger_bands: 0 + - ifish_stoch: 0 + cold: + - bbp: 1 + - bbp: 2 + # - ifish_stoch: 1 + +####### + +#### UPTREND 4h + - label: "DownSTRONG PUMP" + hot: + - bbp: 0 + - stoch_rsi: 0 + - std_uptrend: 0 + # - std_uptrend: 1 + - iip: 0 + - bbp: 1 + - stoch_rsi: 1 + - std_uptrend: 2 + # - std_uptrend: 3 + - bbp: 2 + - stoch_rsi: 2 + - natr: 0 + - roc: 0 + - bollinger_bands: 0 + - stoch_rsi: 3 + # - std_uptrend: 8 + + cold: + - std_uptrend: 4 + # - std_uptrend: 5 + + - label: "DownSTRONG" + hot: + - bbp: 0 + - stoch_rsi: 0 + - std_uptrend: 0 + # - std_uptrend: 1 + - bbp: 1 + - stoch_rsi: 1 + - std_uptrend: 2 + # - std_uptrend: 3 + - bbp: 2 + - stoch_rsi: 2 + - natr: 0 + - roc: 0 + - bollinger_bands: 0 + - stoch_rsi: 3 + # - std_uptrend: 8 + cold: + - std_uptrend: 4 + # - std_uptrend: 5 + + - label: "DownMEDIUM" + hot: + - bbp: 0 + - stoch_rsi: 0 + - std_uptrend: 0 + # - std_uptrend: 1 + - bbp: 1 + - stoch_rsi: 1 + # - std_uptrend: 2 + # - std_uptrend: 3 + - natr: 0 + - roc: 0 + - bollinger_bands: 0 + - stoch_rsi: 3 + # - std_uptrend: 8 + cold: + - bbp: 2 + - std_uptrend: 4 + # - std_uptrend: 5 diff --git a/app/config.yml.bk b/app/config.yml.bk new file mode 100755 index 00000000..bbd90a26 --- /dev/null +++ b/app/config.yml.bk @@ -0,0 +1,766 @@ +# COPIED AND MODIFIED FROM APP/CONFIG.YML +# This is the config.yml to modify + +settings: + log_mode: text + log_level: INFO + enable_charts: false + output_mode: cli + update_interval: 300 + start_worker_interval: 5 + market_data_chunk_size: 70 + # Default timezome UTC + timezone: America/Caracas + market_pairs: + # - 1INCH/USDT + # - AAVE/USDT + - ADA/USDT + # - AGIX/USDT + # - ALGO/USDT + - APE/USDT + - APT/USDT + # - ARB/USDT + - ARKM/USDT + # - ATOM/USDT + # - AUDIO/USDT + # - AVAX/USDT + - AXS/USDT + - BEAMX/USDT + - BNB/USDT + - BONK/USDT + - BTC/USDT + #- BCH/USDT + - CAKE/USDT + - CTXC/USDT + # - BTTC/USDT + # - COTI/USDT + # - DODO/USDT + # - DOT/USDT + # - EGLD/USDT + # - ETC/USDT + - ETH/USDT + # - FET/USDT + - FLOKI/USDT + # - GALA/USDT + # - GLM/USDT + # - GMT/USDT + # - GRT/USDT + # - HBAR/USDT + # - ICP/USDT + # - IMX/USDT + - ILV/USDT + - INJ/USDT + - JTO/USDT + # - LINK/USDT + # - LOKA/USDT + # - LTC/USDT + # - MANA/USDT + # - MANTA/USDT + - MATIC/USDT + - MEME/USDT + # - WLD/USDT + # - ORDI/USDT + - NEAR/USDT + # - OCEAN/USDT + # - VET/USDT + # - OP/USDT + # - STX/USDT + - PENDLE/USDT + - PEPE/USDT + - PYR/USDT + - PYTH/USDT + - RENDER/USDT + - RONIN/USDT + # - ROSE/USDT + # - RUNE/USDT + # - SAND/USDT + # - SEI/USDT + - SLP/USDT + - SOL/USDT + # - SUI/USDT + # - SUSHI/USDT + # - TFUEL/USDT + - THETA/USDT + - TIA/USDT + # - TRX/USDT + # - TWT/USDT + # - UNI/USDT + # - XLM/USDT + # - XRP/USDT + # - YGG/USDT + # - PIXEL/USDT + - WIF/USDT + + +exchanges: + binance: + required: + enabled: true + # all_pairs: + # - USDT + # exclude: + # - USDC + # - PAX + # - USDT + +notifiers: + stdout: + required: + enable: true + optional: + template: "{{ base_currency }} {{ status }} *{{ decimal_format|format(price_value['15m'].close) }}* {{values}}" + # template: " {% if status in ['DT-1h','UT-1h','DT-4h', 'UT-4h','PUMP','DUMP', 'DUMPING', 'PUMPING' ] %} + # *${{base_currency}}* *{{ decimal_format|format(price_value['15m'].close) }}* *{{status}}* + # {% else %} + # *{{base_currency}}* *{{ decimal_format|format(price_value['15m'].close) }}* *{{status}}* {{ '\n' -}} *{{values}}* + # {% endif %}" + + telegram: + required: + enable: true + token: 1737113137:AAFuVKpyYN_Nle0ffnYPNKSXcgg1CnTWROY + chat_id: -834013906 + optional: + parse_mode: markdown + # template: " {% if base_currency in ['BTC','BNB','SOL','AXS','ETH', 'LINK', 'TWT', 'AAVE','SLP','MATIC','XLM'] %} + # *${{base_currency}}* *{{ decimal_format|format(price_value['15m'].close) }}* *{{status}}* + # {% elif status not in ['DT-1h','UT-1h','DT-4h', 'UT-4h','PUMP','DUMP', 'DUMPING', 'PUMPING' ] %} + # *{{base_currency}}* *{{ decimal_format|format(price_value['15m'].close) }}* *{{status}}* + # {% endif %}" + # template: "*${{base_currency}}* *{{ decimal_format|format(price_value['15m'].close) }}* *{{status}}*" + template: " {% if status in ['UT-1st','UT-2nd','DT-1st','DT-2nd','DT-1h','UT-1h','DT-4h', 'UT-4h','PUMP','DUMP'] %} + *${{base_currency}}* *{{status}}* *{{ decimal_format|format(price_value['15m'].close) }}* + {% else %} + *{{base_currency}}* *{{status}}* *{{ decimal_format|format(price_value['15m'].close) }}* {{ '\n' -}} {{values}} + {% endif %}" + +indicators: + rsi: + - enabled: false + + macd: + - enabled: false + + sqzmom: + - enabled: false + alert_enabled: true + alert_frequency: always + candle_period: 4h + signal: + - close + + stoch_rsi: + - enabled: true + candle_period: 15m + alert_enabled: true + alert_frequency: always + cold: 95 + hot: 95 + signal: + - stoch_rsi + - slow_k + # - slow_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI 15m' + mute_cold: false + - enabled: true + candle_period: 1h + alert_enabled: true + alert_frequency: always + cold: 60 + hot: 60 + signal: + - stoch_rsi + - slow_k + # - slow_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI 1h' + mute_cold: false + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: always + cold: 70 + hot: 70 + signal: + - stoch_rsi + - slow_k + # - slow_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI 4h' + mute_cold: false + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: always + cold: 80 + hot: 80 + signal: + - stoch_rsi + - slow_k + # - slow_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 4h' + mute_cold: false + - enabled: true + candle_period: 5m + alert_enabled: true + alert_frequency: always + cold: 99 + hot: 99 + signal: + # - stoch_rsi + - slow_k + - slow_d + + stochrsi_cross: + - enabled: true + candle_period: 15m + alert_enabled: true + alert_frequency: once + cold: 80 + hot: 80 + signal: + # - stoch_rsi + - smooth_k + - smooth_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 15m' + mute_cold: false + - enabled: false + candle_period: 1h + alert_enabled: true + alert_frequency: always + cold: 80 + hot: 80 + signal: + # - stoch_rsi + - smooth_k + - smooth_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 4h' + mute_cold: false + - enabled: false + candle_period: 4h + alert_enabled: true + alert_frequency: always + cold: 70 + hot: 70 + signal: + # - stoch_rsi + - smooth_k + - smooth_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 4h' + mute_cold: false + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: always + cold: 90 + hot: 10 + signal: + # - stoch_rsi + - smooth_k + - smooth_d + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'StochRSI Cross 4h' + mute_cold: false + + iiv: + - enabled: false + + iip: + - enabled: true + alert_enabled: true + alert_frequency: always + signal: + - iip + hot: 1.4 + candle_period: 1h + period_count: 9 + mute_cold: false + + ma_crossover: + - enabled: false + + macd_cross: + - enabled: false + alert_enabled: true + candle_period: 15m + alert_frequency: always + signal: + - macd + - signal + - macdhist + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'MACD Cross 15m' + - enabled: false + alert_enabled: true + candle_period: 1h + alert_frequency: always + signal: + - macd + - signal + - macdhist + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'MACD Cross 50m' + mute_cold: false + - enabled: false + alert_enabled: true + candle_period: 4h + alert_frequency: always + signal: + - macd + - signal + - macdhist + hot_label: 'Uptrend is coming' + cold_label: 'Downtred is coming' + indicator_label: 'MACD Cross 4h' + + ichimoku: + - enabled: false + momentum: + - enabled: false + cci: + - enabled: true + candle_period: 5m + alert_enabled: true + alert_frequency: always + cold: 200 + hot: -200 + signal: + - cci + hot_label: 'Momentum Increasing or OverSold' + cold_label: 'Momentum Decreasing or OverBought' + indicator_label: 'CCI 15m' + mute_cold: false + - enabled: true + candle_period: 15m + alert_enabled: true + alert_frequency: always + cold: 200 + hot: -200 + signal: + - cci + hot_label: 'Momentum Increasing or OverSold' + cold_label: 'Momentum Decreasing or OverBought' + indicator_label: 'CCI 15m' + mute_cold: false + - enabled: true + candle_period: 1h + alert_enabled: true + alert_frequency: always + cold: 200 + hot: -200 + signal: + - cci + hot_label: 'Momentum Increasing or OverSold' + cold_label: 'Momentum Decreasing or OverBought' + indicator_label: 'CCI 1h' + mute_cold: false + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: always + cold: 150 + hot: -150 + signal: + - cci + hot_label: 'Momentum Increasing or OverSold' + cold_label: 'Momentum Decreasing or OverBought' + indicator_label: 'CCI 4h' + mute_cold: false + mfi: + - enabled: false + obv: + - enabled: false + bollinger: + - enabled: false + + bbp: + - enabled: false + candle_period: 15m + alert_enabled: true + alert_frequency: always + period_count: 20 + hot: 0.33 + cold: 0.4 + std_dev: 2 + signal: + - bbp + # - mfi + hot_label: 'Lower Band' + cold_label: 'Upper Band' + indicator_label: 'Bollinger Crossing 15m' + mute_cold: false + - enabled: false + candle_period: 1h + alert_enabled: true + alert_frequency: always + period_count: 20 + hot: 0.5 + cold: 0.5 + std_dev: 2 + signal: + - bbp + # - mfi + hot_label: 'Lower Band' + cold_label: 'Upper Band' + indicator_label: 'Bollinger Crossing 1h' + mute_cold: false + - enabled: false + candle_period: 4h + alert_enabled: true + alert_frequency: always + period_count: 20 + hot: 0.5 + cold: 0.5 + std_dev: 2 + signal: + - bbp + # - mfi + hot_label: 'Lower Band' + cold_label: 'Upper Band' + indicator_label: 'Bollinger Crossing 4h' + mute_cold: false + natr: + - enabled: false + alert_enabled: true + alert_frequency: always + signal: + - natr + candle_period: 15m + period_count: 9 + hot_label: 'Hot' + cold_label: 'Cold' + indicator_label: 'NATR' + mute_cold: false + bollinger_bands: + - enabled: false + alert_enabled: true + alert_frequency: always + signal: + # - upperband + # - middleband + # - lowerband + - bbwidth + candle_period: 15m + period_count: 20 + hot_label: 'Hot' + cold_label: 'Cold' + indicator_label: 'BB' + mute_cold: false + + ifish_stoch: + - enabled: false + alert_enabled: true + alert_frequency: always + signal: + - ifish_stoch + candle_period: 15m + period_count: 5 + hot: -0.9 + cold: 0.9 + hot_label: 'Hot' + cold_label: 'Cold' + indicator_label: 'Ifish Stoch' + mute_cold: false + + stoch_hma_avg_rsi: + - enabled: true + alert_enabled: true + alert_frequency: always + signal: + - stoch_hma_avg_rsi + hot: 50 + cold: 50 + candle_period: 4h + period_count: 3 + mute_cold: false + + stoch_hma: + - enabled: true + alert_enabled: true + alert_frequency: always + signal: + - stoch_hma + hot: 80 + cold: 80 + candle_period: 4h + period_count: 9 + + roc: + - enabled: true + alert_enabled: true + alert_frequency: always + hot: 2 + cold: -2 + signal: + - roc + candle_period: 1h + period_count: 1 + hot_label: 'Hot' + cold_label: 'Cold' + indicator_label: 'ROC' + mute_cold: false + - enabled: false + alert_enabled: true + alert_frequency: always + hot: 0.25 + cold: 0.0 + signal: + - roc + candle_period: 15m + period_count: 2 + hot_label: 'Hot' + cold_label: 'Cold' + indicator_label: 'ROC' + mute_cold: false + + + +informants: + lrsi: + - enabled: false + alert_enabled: true + signal: + - lrsi + candle_period: 15m + hma: + - enabled: false + signal: + - hma + candle_period: 4h + period_count: 3 + vwap: + - enabled: false + sma: + - enabled: false + ema: + - enabled: false + + ohlcv: + - enabled: true + signal: + # - open + # - high + # - low + - close + # - volume + candle_period: 15m + + +crossovers: + std_crossover: + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 2 + key_indicator_type: indicators + key_signal: slow_k + crossed_indicator: stoch_hma_avg_rsi + crossed_indicator_index: 0 + crossed_indicator_type: indicators + crossed_signal: stoch_hma_avg_rsi + + # Crossunder #1 4h + # - enabled: true + # alert_enabled: true + # alert_frequency: once + # key_indicator: stoch_hma_avg_rsi + # key_indicator_index: 0 + # key_indicator_type: indicators + # key_signal: stoch_hma_avg_rsi + # crossed_indicator: stoch_rsi + # crossed_indicator_index: 2 + # crossed_indicator_type: indicators + # crossed_signal: slow_k + +uptrends: + std_uptrend: + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 4 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 1 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: true + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 0 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: false + alert_enabled: true + alert_frequency: always + key_indicator: macd_cross + key_indicator_index: 1 + key_indicator_type: indicators + key_signal: macdhist + key_period_count: 1 + - enabled: false + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 2 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: false + alert_enabled: true + alert_frequency: always + key_indicator: macd_cross + key_indicator_index: 2 + key_indicator_type: indicators + key_signal: macdhist + key_period_count: 1 + - enabled: false + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 1 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: false + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 2 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + - enabled: false + alert_enabled: true + alert_frequency: always + key_indicator: stoch_rsi + key_indicator_index: 3 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 + +conditionals: +# SIGNALS + + + +#### TRENDS + - label: "DT-1st" + cold: + # - std_crossover: 0 + - stochrsi_cross: 0 + - stochrsi_cross: 1 + - stoch_hma: 0 + - cci: 1 + - cci: 2 + - cci: 3 + - std_uptrend: 0 + - std_uptrend: 1 + + + - label: "UT-1st" + hot: + # - std_crossover: 0 + - stochrsi_cross: 0 + - stochrsi_cross: 1 + - stoch_hma: 0 + - cci: 3 + - cci: 1 + - cci: 2 + - std_uptrend: 0 + - std_uptrend: 1 + # - roc: 1 + +#### TRENDS + - label: "DT-2nd" + cold: + - std_crossover: 0 + - stochrsi_cross: 0 + - stoch_hma: 0 + - cci: 3 + - cci: 1 + - cci: 2 + - std_uptrend: 0 + - std_uptrend: 1 + + - label: "UT-2nd" + hot: + - std_crossover: 0 + - stochrsi_cross: 0 + - stoch_hma: 0 + - cci: 3 + - cci: 1 + - cci: 2 + - std_uptrend: 0 + - std_uptrend: 1 + # - roc: 1 + +### PUMP DUMP + + # - label: "PUMP" + # hot: + # - iip: 0 + # - roc: 0 + + # - label: "DUMP" + # cold: + # - iip: 0 + # - roc: 0 + + - label: "PUMP2short" + hot: + - iip: 0 + - roc: 0 + cold: + - std_uptrend: 0 + - std_uptrend: 2 + - cci: 1 + + - label: "PUMP2long" + hot: + - iip: 0 + - roc: 0 + - std_uptrend: 0 + - std_uptrend: 2 + - cci: 1 + + - label: "DUMP2long" + cold: + - iip: 0 + - roc: 0 + - std_uptrend: 0 + - std_uptrend: 2 + - cci: 1 + + - label: "DUMP2short" + hot: + - std_uptrend: 0 + - std_uptrend: 2 + - cci: 1 + cold: + - iip: 0 + - roc: 0 diff --git a/app/defaults.yml b/app/defaults.yml old mode 100644 new mode 100755 index ac5fda9a..bac4c39d --- a/app/defaults.yml +++ b/app/defaults.yml @@ -75,6 +75,19 @@ indicators: cold: 0 candle_period: 1d period_count: 10 + cci: + - enabled: true + candle_period: 4h + alert_enabled: true + alert_frequency: always + cold: 100 + hot: -100 + signal: + - cci + hot_label: 'Momentum Increasing or OverSold' + cold_label: 'Momentum Decreasing or OverBought' + indicator_label: 'CCI 4h' + mute_cold: false mfi: - enabled: true alert_enabled: true @@ -114,6 +127,26 @@ indicators: cold: 80 candle_period: 1d period_count: 14 + stoch_hma_avg_rsi: + - enabled: true + alert_enabled: true + alert_frequency: once + signal: + - stoch_hma_avg_rsi + hot: 20 + cold: 80 + candle_period: 1d + period_count: 14 + stoch_hma: + - enabled: true + alert_enabled: true + alert_frequency: once + signal: + - stoch_hma + hot: 50 + cold: 50 + candle_period: 4h + period_count: 9 macd: - enabled: true alert_enabled: true @@ -142,6 +175,32 @@ indicators: hot: 5 cold: 0 candle_period: 5m + natr: + - enabled: true + signal: + - natr + candle_period: 1d + period_count: 15 + roc: + - enabled: true + signal: + - roc + candle_period: 1d + period_count: 15 + bollinger_bands: + - enabled: true + signal: + - upperband + - middleband + - lowerband + - bbwidth + candle_period: 1d + ifish_stoch: + - enabled: true + signal: + - ifish_stoch + candle_period: 1d + informants: lrsi: - enabled: true @@ -172,13 +231,12 @@ informants: - ema candle_period: 1d period_count: 15 - bollinger_bands: + hma: - enabled: true signal: - - upperband - - middleband - - lowerband + - hma candle_period: 1d + period_count: 9 ohlcv: - enabled: true signal: @@ -186,6 +244,7 @@ informants: candle_period: 1d period_count: 15 + crossovers: std_crossover: - enabled: false @@ -199,3 +258,14 @@ crossovers: crossed_indicator_index: 0 crossed_indicator_type: informants crossed_signal: sma + +uptrends: + std_uptrend: + - enabled: false + alert_enabled: true + alert_frequency: once + key_indicator: ema + key_indicator_index: 0 + key_indicator_type: informants + key_signal: ema + key_period_count: 1 diff --git a/app/exchange.py b/app/exchange.py old mode 100644 new mode 100755 index 7cf8cbef..025354cc --- a/app/exchange.py +++ b/app/exchange.py @@ -57,7 +57,7 @@ def __init__(self, exchange_config): "Unable to load exchange %s", new_exchange) @retry(retry=retry_if_exception_type(ccxt.NetworkError), stop=stop_after_attempt(3)) - def get_historical_data(self, market_pair, exchange, time_unit, start_date=None, max_periods=240): + def get_historical_data(self, market_pair, exchange, time_unit, start_date=None, max_periods=50): """Get historical OHLCV for a symbol pair Decorators: @@ -68,7 +68,7 @@ def get_historical_data(self, market_pair, exchange, time_unit, start_date=None, exchange (str): Contains the exchange to fetch the historical data from. time_unit (str): A string specifying the ccxt time unit i.e. 5m or 1d. start_date (int, optional): Timestamp in milliseconds. - max_periods (int, optional): Defaults to 100. Maximum number of time periods + max_periods (int, optional): Defaults to 50. Maximum number of time periods back to fetch data for. Returns: diff --git a/app/logs.py b/app/logs.py old mode 100644 new mode 100755 diff --git a/app/notification.py b/app/notification.py index 418c4387..3fa9b399 100755 --- a/app/notification.py +++ b/app/notification.py @@ -220,11 +220,14 @@ def notify_conditional(self, exchange, market_pair, messages): msg['indicator']) c_nb_conditions += 1 if alert_frequency != 'once': - key = ''.join([msg['market'], list( - msg['values'])[0], candle_period]) + # print(msg) + # deleted list(msg['values'])[0] in join out of index list uptrend + # values + key = ''.join([msg['market'], list(msg['values'])[0], candle_period]) should_alert += self.should_i_alert( key, alert_frequency) - if msg['status'] == msg['last_status'] and alert_frequency == 'once' and not self.first_run: + if msg['status'] == msg[ + 'last_status'] and alert_frequency == 'once' and not self.first_run: c_nb_once_muted += 1 if msg['status'] != msg['last_status']: c_nb_new_status += 1 @@ -480,6 +483,19 @@ def _indicator_message_templater(self, new_analysis, template): values[crossed_signal] = format( values[crossed_signal], '.8f') + elif indicator_type == 'uptrends': + latest_result = analysis['result'].iloc[-1] + + key_signal = '{}_{}'.format( + analysis['config']['key_signal'], + analysis['config']['key_indicator_index'] + ) + + values[key_signal] = analysis['result'].iloc[-1][key_signal] + if isinstance(values[key_signal], float): + values[key_signal] = format( + values[key_signal], '.8f') + status = 'neutral' if latest_result['is_hot']: status = 'hot' @@ -594,17 +610,20 @@ def get_indicator_messages(self, new_analysis): new_messages = dict() ohlcv_values = dict() lrsi_values = dict() + hma_values = dict() for exchange in new_analysis: new_messages[exchange] = dict() ohlcv_values[exchange] = dict() lrsi_values[exchange] = dict() + hma_values[exchange] = dict() for market_pair in new_analysis[exchange]: new_messages[exchange][market_pair] = dict() ohlcv_values[exchange][market_pair] = dict() lrsi_values[exchange][market_pair] = dict() + hma_values[exchange][market_pair] = dict() # Getting price values for each market pair and candle period for indicator_type in new_analysis[exchange][market_pair]: @@ -615,7 +634,7 @@ def get_indicator_messages(self, new_analysis): for signal in analysis['config']['signal']: values[signal] = analysis['result'].iloc[-1][signal] ohlcv_values[exchange][market_pair][analysis['config'] - ['candle_period']] = values + ['candle_period']] = values # Getting LRSI values if 'lrsi' in new_analysis[exchange][market_pair]['informants']: @@ -625,14 +644,25 @@ def get_indicator_messages(self, new_analysis): values[signal] = analysis['result'].iloc[-1][signal] lrsi_values[exchange][market_pair][analysis['config'] - ['candle_period']] = values + ['candle_period']] = values + + # Getting HMA values + if 'hma' in new_analysis[exchange][market_pair]['informants']: + for index, analysis in enumerate(new_analysis[exchange][market_pair]['informants']['hma']): + values = dict() + for signal in analysis['config']['signal']: + values[signal] = analysis['result'].iloc[-1][signal] + + hma_values[exchange][market_pair][analysis['config'] + ['candle_period']] = values for indicator_type in new_analysis[exchange][market_pair]: if indicator_type == 'informants': continue for indicator in new_analysis[exchange][market_pair][indicator_type]: - for index, analysis in enumerate(new_analysis[exchange][market_pair][indicator_type][indicator]): + for index, analysis in enumerate( + new_analysis[exchange][market_pair][indicator_type][indicator]): if analysis['result'].shape[0] == 0: continue @@ -676,6 +706,19 @@ def get_indicator_messages(self, new_analysis): values[crossed_signal] = format( values[crossed_signal], '.2f') + elif indicator_type == 'uptrends': + latest_result = analysis['result'].iloc[-1] + + key_signal = '{}_{}'.format( + analysis['config']['key_signal'], + analysis['config']['key_indicator_index'] + ) + + values[key_signal] = analysis['result'].iloc[-1][key_signal] + if isinstance(values[key_signal], float): + values[key_signal] = format( + values[key_signal], '.2f') + status = 'neutral' if latest_result['is_hot']: status = 'hot' @@ -714,12 +757,14 @@ def get_indicator_messages(self, new_analysis): should_alert = False else: should_alert = self.should_i_alert(''.join( - [market_pair, indicator, candle_period]), analysis['config']['alert_frequency']) + [market_pair, indicator, candle_period]), + analysis['config']['alert_frequency']) if not analysis['config']['alert_enabled']: should_alert = False - if 'mute_cold' in analysis['config'] and analysis['config']['mute_cold'] == True and latest_result['is_cold'] == True: + if 'mute_cold' in analysis['config'] and analysis['config']['mute_cold'] == True and \ + latest_result['is_cold'] == True: self.logger.info( 'Skiping cold notification for %s %s %s', market_pair, indicator, candle_period) should_alert = False @@ -753,6 +798,11 @@ def get_indicator_messages(self, new_analysis): if candle_period in lrsi_values[exchange][market_pair]: lrsi = lrsi_values[exchange][market_pair][candle_period]['lrsi'] lrsi = format(lrsi, '.2f') + + hma = '' + if candle_period in hma_values[exchange][market_pair]: + hma = hma_values[exchange][market_pair][candle_period]['hma'] + hma = format(hma, '.2f') """ new_message = message_template.render( @@ -767,11 +817,14 @@ def get_indicator_messages(self, new_analysis): del analysis['result'] new_message = dict( - values=values, exchange=exchange, market=market_pair, base_currency=base_currency, + values=values, exchange=exchange, market=market_pair, + base_currency=base_currency, quote_currency=quote_currency, indicator=indicator, indicator_number=index, analysis=analysis, status=status, last_status=last_status, - prices=prices, lrsi=lrsi, creation_date=creation_date, hot_cold_label=hot_cold_label, - indicator_label=indicator_label, price_value=price_value, decimal_format=decimal_format) + prices=prices, lrsi=lrsi, hma=hma, creation_date=creation_date, + hot_cold_label=hot_cold_label, + indicator_label=indicator_label, price_value=price_value, + decimal_format=decimal_format) new_messages[exchange][market_pair][candle_period].append( new_message) @@ -1043,7 +1096,7 @@ def plot_candlestick(self, ax, df, candle_period, candle_pattern): ma25 = self.EMA(df, 25) ma99 = self.EMA(df, 99) - if(df['close'].count() > 120): + if (df['close'].count() > 120): df = df.iloc[-120:] ma7 = ma7.iloc[-120:] ma25 = ma25.iloc[-120:] @@ -1073,9 +1126,9 @@ def plot_candlestick(self, ax, df, candle_period, candle_pattern): ax.text(0.04, 0.94, 'EMA (7, close)', color='darkorange', transform=ax.transAxes, fontsize=textsize, va='top') ax.text(0.24, 0.94, 'EMA (25, close)', color='mediumslateblue', - transform=ax.transAxes, fontsize=textsize, va='top') + transform=ax.transAxes, fontsize=textsize, va='top') ax.text(0.46, 0.94, 'EMA (99, close)', color='firebrick', - transform=ax.transAxes, fontsize=textsize, va='top') + transform=ax.transAxes, fontsize=textsize, va='top') def plot_rsi(self, ax, df): textsize = 11 @@ -1083,7 +1136,7 @@ def plot_rsi(self, ax, df): rsi = self.relative_strength(df["close"]) - if(df['close'].count() > 120): + if (df['close'].count() > 120): df = df.iloc[-120:] rsi = rsi[-120:] @@ -1105,7 +1158,7 @@ def plot_macd(self, ax, df, candle_period): df = StockDataFrame.retype(df) df['macd'] = df.get('macd') - if(df['macd'].count() > 120): + if (df['macd'].count() > 120): df = df.iloc[-120:] min_y = df.macd.min() @@ -1213,7 +1266,8 @@ def moving_average(self, x, n, type='simple'): return a def EMA(self, df, n, field='close'): - return pd.Series(talib.EMA(df[field].astype('f8').values, n), name='EMA_' + field.upper() + '_' + str(n), index=df.index) + return pd.Series(talib.EMA(df[field].astype('f8').values, n), name='EMA_' + field.upper() + '_' + str(n), + index=df.index) def plot_ichimoku(self, ax, df, historical_data, candle_period): indicator_conf = {} @@ -1226,13 +1280,14 @@ def plot_ichimoku(self, ax, df, historical_data, candle_period): tenkansen_period = indicator_conf['tenkansen_period'] if 'tenkansen_period' in indicator_conf else 20 kijunsen_period = indicator_conf['kijunsen_period'] if 'kijunsen_period' in indicator_conf else 60 - senkou_span_b_period = indicator_conf['senkou_span_b_period'] if 'senkou_span_b_period' in indicator_conf else 120 + senkou_span_b_period = indicator_conf[ + 'senkou_span_b_period'] if 'senkou_span_b_period' in indicator_conf else 120 textsize = 11 ichimoku_data = ichimoku.Ichimoku().analyze(historical_data, tenkansen_period, kijunsen_period, senkou_span_b_period, chart=True) - if(df['close'].count() > 120): + if (df['close'].count() > 120): df = df.iloc[-120:] ##change 146 if cloud displacement period changed in ichimoku.Ichimoku().calculate()## ichimoku_data = ichimoku_data.iloc[-146:] diff --git a/app/notifiers/__init__.py b/app/notifiers/__init__.py old mode 100644 new mode 100755 diff --git a/app/notifiers/discord_client.py b/app/notifiers/discord_client.py old mode 100644 new mode 100755 diff --git a/app/notifiers/email_client.py b/app/notifiers/email_client.py old mode 100644 new mode 100755 diff --git a/app/notifiers/slack_client.py b/app/notifiers/slack_client.py old mode 100644 new mode 100755 diff --git a/app/notifiers/stdout_client.py b/app/notifiers/stdout_client.py old mode 100644 new mode 100755 diff --git a/app/notifiers/telegram_client.py b/app/notifiers/telegram_client.py old mode 100644 new mode 100755 diff --git a/app/notifiers/twilio_client.py b/app/notifiers/twilio_client.py old mode 100644 new mode 100755 diff --git a/app/notifiers/utils.py b/app/notifiers/utils.py old mode 100644 new mode 100755 diff --git a/app/notifiers/webhook_client.py b/app/notifiers/webhook_client.py old mode 100644 new mode 100755 diff --git a/app/outputs.py b/app/outputs.py old mode 100644 new mode 100755 index cb10180b..db230481 --- a/app/outputs.py +++ b/app/outputs.py @@ -85,6 +85,33 @@ def to_cli(self, results, market_pair): formatted_string, normal_colour ) + + elif indicator_type == 'uptrends': + key_signal = '{}_{}'.format( + analysis['config']['key_signal'], + analysis['config']['key_indicator_index'] + ) + + key_value = analysis['result'].iloc[-1][key_signal] + p = int(analysis['config']['key_period_count']) + 1 + uptrended_value = analysis['result'].iloc[-p][key_signal] + + if isinstance(key_value, float): + key_value = format(key_value, '.8f') + + if isinstance(uptrended_value, float): + uptrended_value = format(uptrended_value, '.8f') + + formatted_string = '{}/{}'.format( + key_value, uptrended_value) + output += "{}{}: {}{} \t".format( + colour_code, + '{} #{}'.format(indicator, i), + formatted_string, + normal_colour + ) + + else: formatted_values = list() for signal in analysis['config']['signal']: diff --git a/app/requirements-step-1.txt b/app/requirements-step-1.txt old mode 100644 new mode 100755 index 1bd9e5ab..7ffff681 --- a/app/requirements-step-1.txt +++ b/app/requirements-step-1.txt @@ -1,2 +1,2 @@ -numpy>=1.14.0 -Cython>=0.28.2 +numpy>=1.26.2 +Cython>=3.0.5 diff --git a/app/requirements-step-2.txt b/app/requirements-step-2.txt old mode 100644 new mode 100755 index 54fdae55..8a0a06d9 --- a/app/requirements-step-2.txt +++ b/app/requirements-step-2.txt @@ -1,18 +1,23 @@ -twilio>=6.6.3 -ccxt>=1.25.40 -structlog>=17.2.0 -python-json-logger>=0.1.8 -pandas>=0.22.0 -stockstats>=0.2.0 -TA-lib>=0.4.17 -tabulate>=0.8.2 +twilio>=6.73.0 +ccxt>=2.2.28 +structlog>=18.4.0 +python-json-logger>=0.1.9 +pandas==2.1.3 +stockstats>=0.5.2 +TA-lib>=0.4.28 +tabulate>=0.8.7 slackweb>=1.0.5 -tenacity>=4.8.0 -python-telegram-bot>=10.0.1 -discord-webhook>=0.15.0 -jinja2>=2.10 -requests>=2.20.0 -PyYAML>=5.1 -newtulipy -matplotlib>=3.0.1 -scipy>=1.1.0 +tenacity>=7.1.0 +python-telegram-bot==10.0.1 +discord-webhook>=1.3.0 +jinja2==3.0.0 +requests>=2.28.1 +PyYAML>=6.0 +newtulipy>=0.4.6 +matplotlib>=3.8.2 +scipy>=1.11.4 +python-dateutil>=2.8.2 +pytz>=2023.3 +six>=1.16.0 +pandas-ta==0.3.14b +setuptools>=69.0.2 \ No newline at end of file diff --git a/default-config.yml b/default-config.yml old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml old mode 100644 new mode 100755 index 1502221a..0892a8c7 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,9 @@ -version: '3' - +version: '3.8' services: - app: + crypto-signal_app: build: . + image: crypto-signal_app:latest + restart: "always" + container_name: cryptosignal + volumes: + - "./app:/app" diff --git a/docs/config.md b/docs/config.md old mode 100644 new mode 100755 index 29c45df5..13db2884 --- a/docs/config.md +++ b/docs/config.md @@ -7,9 +7,10 @@ 5) Indicators 6) Informants 7) Crossovers -8) Conditionals -9) Advanced Settings -10) Examples +8) Uptrends +9) Conditionals +10) Advanced Settings +11) Examples # 1) Configuration file structure The configuration file is YAML formatted and consists of the following top level keys. @@ -20,6 +21,7 @@ The configuration file is YAML formatted and consists of the following top level - indicators - informants - crossovers +- uptrends You will find a detailed description of each key below @@ -395,6 +397,7 @@ description: Valid values are on a per indicator basis see the table below. Each KLINGER OSCILLATOR - kvo, kvo_signal ADX - adx MOMENTUM - momentum +CCI - cci RSI - rsi MACD - macd, macdsignal, macdhist ICHIMOKU - leading_span_a, leading_span_b @@ -533,7 +536,7 @@ necessity: optional\ description: Valid values are the name of a signal line for the select indicator or informant. Which signal to use of the selected indicator or informant. -An example of configuring an informant would look as follows: +An example of configuring a informant would look as follows: ```yml crossovers: @@ -551,7 +554,62 @@ crossovers: crossed_signal: sma ``` -# 8) Conditionals +# 8) Uptrends + +**enabled**\ +default: False\ +necessity: optional\ +description: Valid values are true or false. Whether to perform analysis on this indicator. + +**alert_enabled**\ +default: False\ +necessity: optional\ +description: Valid values are true or false. Whether to send alerts for this particular indicator. + +**key_indicator**\ +default: N/A\ +necessity: optional\ +description: Valid values are the name of any indicator or informant. The indicator that gets hot when it goes above the crossed indicator and cold when it goes below it. + +**key_indicator_index**\ +default: N/A\ +necessity: optional\ +description: Valid values are positive integers. The index of the selected key indicator or informant that you want to use. + +**key_indicator_type**\ +default: N/A\ +necessity: optional\ +description: Valid values are 'indicators' or 'informants'. Whether the key indicator is of type informant or indicator. + +**key_signal**\ +default: N/A\ +necessity: optional\ +description: Valid values are the name of a signal line for the select indicator or informant. Which signal to use of the selected indicator or informant. + +**key_period_count**\ +default: 1\ +necessity: optional\ +description: Position of period back to be evaluated + + + +An example of configuring an indicator would look as follows: + +```yml +uptrends: + std_uptrend: + - enabled: enable + alert_enabled: true + alert_frequency: once + key_indicator: stoch_rsi + key_indicator_index: 0 + key_indicator_type: indicators + key_signal: slow_k + key_period_count: 1 +``` + + +# 9) Conditionals It's allowing you to receive notifications, only if one or more conditions are respected. @@ -672,7 +730,7 @@ notifiers: The `status` will be the string set in `label`. -# 9) Advanced Settings +# 10) Advanced Settings ## `start_worker_interval` `start_worker_interval` allows to define the number of the seconds between each start of a worker (use of multi processing to manage chunk of pairs). It's usefull to manage time between requests to exchange servers. @@ -693,7 +751,7 @@ notifiers: [...] ``` -# 10) Examples +# 11) Examples Putting it all together an example config.yml might look like the config below if you want to use the default settings with bittrex ```yml diff --git a/lib/ta-lib-0.4.0-src.tar.gz b/lib/ta-lib-0.4.0-src.tar.gz old mode 100644 new mode 100755