diff --git a/.gitignore b/.gitignore index c81b55222..7c7102874 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ config.json *.sqlite .hyperopt logfile.txt +hyperopt_trials.pickle +user_data/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -85,5 +87,3 @@ target/ .venv .idea .vscode - -hyperopt_trials.pickle diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index fc2395feb..0481c7f3c 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -7,11 +7,10 @@ from enum import Enum from typing import Dict, List import arrow -import talib.abstract as ta from pandas import DataFrame, to_datetime -import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.exchange import get_ticker_history +from freqtrade.strategy.strategy import Strategy logger = logging.getLogger(__name__) @@ -46,182 +45,8 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame: you are using. Let uncomment only the indicator you are using in your strategies or your hyperopt configuration, otherwise you will waste your memory and CPU usage. """ - - # Momentum Indicator - # ------------------------------------ - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # Awesome oscillator - dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) - """ - # Commodity Channel Index: values Oversold:<-100, Overbought:>100 - dataframe['cci'] = ta.CCI(dataframe) - """ - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['macdhist'] = macd['macdhist'] - - # MFI - dataframe['mfi'] = ta.MFI(dataframe) - - # Minus Directional Indicator / Movement - dataframe['minus_dm'] = ta.MINUS_DM(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # Plus Directional Indicator / Movement - dataframe['plus_dm'] = ta.PLUS_DM(dataframe) - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - """ - # ROC - dataframe['roc'] = ta.ROC(dataframe) - """ - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - """ - # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) - rsi = 0.1 * (dataframe['rsi'] - 50) - dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) - - # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) - dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) - - # Stoch - stoch = ta.STOCH(dataframe) - dataframe['slowd'] = stoch['slowd'] - dataframe['slowk'] = stoch['slowk'] - """ - # Stoch fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - """ - # Stoch RSI - stoch_rsi = ta.STOCHRSI(dataframe) - dataframe['fastd_rsi'] = stoch_rsi['fastd'] - dataframe['fastk_rsi'] = stoch_rsi['fastk'] - """ - - # Overlap Studies - # ------------------------------------ - - # Previous Bollinger bands - # Because ta.BBANDS implementation is broken with small numbers, it actually - # returns middle band for all the three bands. Switch to qtpylib.bollinger_bands - # and use middle band instead. - dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband'] - """ - # Bollinger bands - """ - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_middleband'] = bollinger['mid'] - dataframe['bb_upperband'] = bollinger['upper'] - - # EMA - Exponential Moving Average - dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) - dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) - dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) - - # SAR Parabol - dataframe['sar'] = ta.SAR(dataframe) - - # SMA - Simple Moving Average - dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - - # TEMA - Triple Exponential Moving Average - dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) - - # Cycle Indicator - # ------------------------------------ - # Hilbert Transform Indicator - SineWave - hilbert = ta.HT_SINE(dataframe) - dataframe['htsine'] = hilbert['sine'] - dataframe['htleadsine'] = hilbert['leadsine'] - - # Pattern Recognition - Bullish candlestick patterns - # ------------------------------------ - """ - # Hammer: values [0, 100] - dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) - - # Inverted Hammer: values [0, 100] - dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) - - # Dragonfly Doji: values [0, 100] - dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) - - # Piercing Line: values [0, 100] - dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] - - # Morningstar: values [0, 100] - dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] - - # Three White Soldiers: values [0, 100] - dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] - """ - - # Pattern Recognition - Bearish candlestick patterns - # ------------------------------------ - """ - # Hanging Man: values [0, 100] - dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) - - # Shooting Star: values [0, 100] - dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) - - # Gravestone Doji: values [0, 100] - dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) - - # Dark Cloud Cover: values [0, 100] - dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) - - # Evening Doji Star: values [0, 100] - dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) - - # Evening Star: values [0, 100] - dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) - """ - - # Pattern Recognition - Bullish/Bearish candlestick patterns - # ------------------------------------ - """ - # Three Line Strike: values [0, -100, 100] - dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) - - # Spinning Top: values [0, -100, 100] - dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] - - # Engulfing: values [0, -100, 100] - dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] - - # Harami: values [0, -100, 100] - dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] - - # Three Outside Up/Down: values [0, -100, 100] - dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] - - # Three Inside Up/Down: values [0, -100, 100] - dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - """ - - # Chart type - # ------------------------------------ - # Heikinashi stategy - heikinashi = qtpylib.heikinashi(dataframe) - dataframe['ha_open'] = heikinashi['open'] - dataframe['ha_close'] = heikinashi['close'] - dataframe['ha_high'] = heikinashi['high'] - dataframe['ha_low'] = heikinashi['low'] - - return dataframe + strategy = Strategy() + return strategy.populate_indicators(dataframe=dataframe) def populate_buy_trend(dataframe: DataFrame) -> DataFrame: @@ -230,20 +55,8 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: :param dataframe: DataFrame :return: DataFrame with buy column """ - dataframe.loc[ - ( - (dataframe['rsi'] < 35) & - (dataframe['fastd'] < 35) & - (dataframe['adx'] > 30) & - (dataframe['plus_di'] > 0.5) - ) | - ( - (dataframe['adx'] > 65) & - (dataframe['plus_di'] > 0.5) - ), - 'buy'] = 1 - - return dataframe + strategy = Strategy() + return strategy.populate_buy_trend(dataframe=dataframe) def populate_sell_trend(dataframe: DataFrame) -> DataFrame: @@ -252,21 +65,8 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame: :param dataframe: DataFrame :return: DataFrame with buy column """ - dataframe.loc[ - ( - ( - (qtpylib.crossed_above(dataframe['rsi'], 70)) | - (qtpylib.crossed_above(dataframe['fastd'], 70)) - ) & - (dataframe['adx'] > 10) & - (dataframe['minus_di'] > 0) - ) | - ( - (dataframe['adx'] > 70) & - (dataframe['minus_di'] > 0.5) - ), - 'sell'] = 1 - return dataframe + strategy = Strategy() + return strategy.populate_sell_trend(dataframe=dataframe) def analyze_ticker(ticker_history: List[Dict]) -> DataFrame: diff --git a/freqtrade/main.py b/freqtrade/main.py index 095f7671a..9cce277e1 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -19,6 +19,7 @@ from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.misc import (State, get_state, load_config, parse_args, throttle, update_state) from freqtrade.persistence import Trade +from freqtrade.strategy.strategy import Strategy logger = logging.getLogger('freqtrade') @@ -235,14 +236,16 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) - Based an earlier trade and current price and ROI configuration, decides whether bot should sell :return True if bot should sell at current rate """ + strategy = Strategy() + current_profit = trade.calc_profit_percent(current_rate) - if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']): + if strategy.stoploss is not None and current_profit < float(strategy.stoploss): logger.debug('Stop loss hit.') return True # Check if time matches and current rate is above threshold time_diff = (current_time - trade.open_date).total_seconds() / 60 - for duration, threshold in sorted(_CONF['minimal_roi'].items()): + for duration, threshold in sorted(strategy.minimal_roi.items()): if time_diff > float(duration) and current_profit > threshold: return True @@ -378,6 +381,9 @@ def init(config: dict, db_url: Optional[str] = None) -> None: persistence.init(config, db_url) exchange.init(config) + strategy = Strategy() + strategy.init(config) + # Set initial application state initial_state = config.get('initial_state') if initial_state: @@ -445,6 +451,9 @@ def main(sysargv=sys.argv[1:]) -> None: # Load and validate configuration _CONF = load_config(args.config) + # Add the strategy file to use + _CONF.update({'strategy': args.strategy}) + # Initialize all modules and start main loop if args.dynamic_whitelist: logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)') @@ -462,6 +471,7 @@ def main(sysargv=sys.argv[1:]) -> None: try: init(_CONF) old_state = None + while True: new_state = get_state() # Log state transition diff --git a/freqtrade/misc.py b/freqtrade/misc.py index cab861044..a0669ae19 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -124,6 +124,14 @@ def common_args_parser(description: str): type=str, metavar='PATH', ) + parser.add_argument( + '-s', '--strategy', + help='specify strategy file (default: freqtrade/strategy/default_strategy.py)', + dest='strategy', + default='.default_strategy', + type=str, + metavar='PATH', + ) return parser @@ -380,7 +388,6 @@ CONF_SCHEMA = { 'stake_amount', 'fiat_display_currency', 'dry_run', - 'minimal_roi', 'bid_strategy', 'telegram' ] diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 25e1e6231..93ff295c5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -14,6 +14,7 @@ from freqtrade.analyze import populate_buy_trend, populate_sell_trend from freqtrade.exchange import Bittrex from freqtrade.main import min_roi_reached from freqtrade.persistence import Trade +from freqtrade.strategy.strategy import Strategy logger = logging.getLogger(__name__) @@ -199,6 +200,11 @@ def start(args): logger.info('Using max_open_trades: %s ...', config['max_open_trades']) max_open_trades = config['max_open_trades'] + # init the strategy to use + config.update({'strategy': args.strategy}) + strategy = Strategy() + strategy.init(config) + # Monkey patch config from freqtrade import main main._CONF = config diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b9780c13a..041f9a7a4 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -7,11 +7,10 @@ import sys import pickle import signal import os -from functools import reduce from math import exp from operator import itemgetter -from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe +from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, space_eval, tpe from hyperopt.mongoexp import MongoTrials from pandas import DataFrame @@ -21,7 +20,7 @@ from freqtrade.exchange import Bittrex from freqtrade.misc import load_config from freqtrade.optimize.backtesting import backtest from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf -from freqtrade.vendor.qtpylib.indicators import crossed_above +from freqtrade.strategy.strategy import Strategy # Remove noisy log messages logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING) @@ -57,63 +56,6 @@ from freqtrade import main # noqa main._CONF = OPTIMIZE_CONFIG -SPACE = { - 'macd_below_zero': hp.choice('macd_below_zero', [ - {'enabled': False}, - {'enabled': True} - ]), - 'mfi': hp.choice('mfi', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)} - ]), - 'fastd': hp.choice('fastd', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)} - ]), - 'adx': hp.choice('adx', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)} - ]), - 'rsi': hp.choice('rsi', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)} - ]), - 'uptrend_long_ema': hp.choice('uptrend_long_ema', [ - {'enabled': False}, - {'enabled': True} - ]), - 'uptrend_short_ema': hp.choice('uptrend_short_ema', [ - {'enabled': False}, - {'enabled': True} - ]), - 'over_sar': hp.choice('over_sar', [ - {'enabled': False}, - {'enabled': True} - ]), - 'green_candle': hp.choice('green_candle', [ - {'enabled': False}, - {'enabled': True} - ]), - 'uptrend_sma': hp.choice('uptrend_sma', [ - {'enabled': False}, - {'enabled': True} - ]), - 'trigger': hp.choice('trigger', [ - {'type': 'lower_bb'}, - {'type': 'lower_bb_tema'}, - {'type': 'faststoch10'}, - {'type': 'ao_cross_zero'}, - {'type': 'ema3_cross_ema10'}, - {'type': 'macd_cross_signal'}, - {'type': 'sar_reversal'}, - {'type': 'ht_sine'}, - {'type': 'heiken_reversal_bull'}, - {'type': 'di_cross'}, - ]), - 'stoploss': hp.uniform('stoploss', -0.5, -0.02), -} - - def save_trials(trials, trials_path=TRIALS_FILE): """Save hyperopt trials to file""" logger.info('Saving Trials to \'{}\''.format(trials_path)) @@ -162,7 +104,9 @@ def optimizer(params): global _CURRENT_TRIES from freqtrade.optimize import backtesting - backtesting.populate_buy_trend = buy_strategy_generator(params) + + strategy = Strategy() + backtesting.populate_buy_trend = strategy.buy_strategy_generator(params) results = backtest({'stake_amount': OPTIMIZE_CONFIG['stake_amount'], 'processed': PROCESSED, @@ -208,59 +152,8 @@ def format_results(results: DataFrame): results.duration.mean() * 5, ) - -def buy_strategy_generator(params): - def populate_buy_trend(dataframe: DataFrame) -> DataFrame: - conditions = [] - # GUARDS AND TRENDS - if params['uptrend_long_ema']['enabled']: - conditions.append(dataframe['ema50'] > dataframe['ema100']) - if params['macd_below_zero']['enabled']: - conditions.append(dataframe['macd'] < 0) - if params['uptrend_short_ema']['enabled']: - conditions.append(dataframe['ema5'] > dataframe['ema10']) - if params['mfi']['enabled']: - conditions.append(dataframe['mfi'] < params['mfi']['value']) - if params['fastd']['enabled']: - conditions.append(dataframe['fastd'] < params['fastd']['value']) - if params['adx']['enabled']: - conditions.append(dataframe['adx'] > params['adx']['value']) - if params['rsi']['enabled']: - conditions.append(dataframe['rsi'] < params['rsi']['value']) - if params['over_sar']['enabled']: - conditions.append(dataframe['close'] > dataframe['sar']) - if params['green_candle']['enabled']: - conditions.append(dataframe['close'] > dataframe['open']) - if params['uptrend_sma']['enabled']: - prevsma = dataframe['sma'].shift(1) - conditions.append(dataframe['sma'] > prevsma) - - # TRIGGERS - triggers = { - 'lower_bb': (dataframe['close'] < dataframe['bb_lowerband']), - 'lower_bb_tema': (dataframe['tema'] < dataframe['bb_lowerband']), - 'faststoch10': (crossed_above(dataframe['fastd'], 10.0)), - 'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)), - 'ema3_cross_ema10': (crossed_above(dataframe['ema3'], dataframe['ema10'])), - 'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])), - 'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])), - 'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])), - 'heiken_reversal_bull': (crossed_above(dataframe['ha_close'], dataframe['ha_open'])) & - (dataframe['ha_low'] == dataframe['ha_open']), - 'di_cross': (crossed_above(dataframe['plus_di'], dataframe['minus_di'])), - } - conditions.append(triggers.get(params['trigger']['type'])) - - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 - - return dataframe - return populate_buy_trend - - def start(args): - global TOTAL_TRIES, PROCESSED, SPACE, TRIALS, _CURRENT_TRIES + global TOTAL_TRIES, PROCESSED, TRIALS, _CURRENT_TRIES TOTAL_TRIES = args.epochs @@ -275,6 +168,12 @@ def start(args): logger.info('Using config: %s ...', args.config) config = load_config(args.config) pairs = config['exchange']['pair_whitelist'] + + # init the strategy to use + config.update({'strategy': args.strategy}) + strategy = Strategy() + strategy.init(config) + timerange = misc.parse_timerange(args.timerange) data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval, @@ -303,7 +202,7 @@ def start(args): try: best_parameters = fmin( fn=optimizer, - space=SPACE, + space=strategy.hyperopt_space(), algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=TRIALS @@ -319,7 +218,10 @@ def start(args): # Improve best parameter logging display if best_parameters: - best_parameters = space_eval(SPACE, best_parameters) + best_parameters = space_eval( + strategy.hyperopt_space(), + best_parameters + ) logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4)) logger.info('Best Result:\n%s', best_result) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py new file mode 100644 index 000000000..670cc2abe --- /dev/null +++ b/freqtrade/strategy/default_strategy.py @@ -0,0 +1,262 @@ +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.strategy.interface import IStrategy +from pandas import DataFrame +from hyperopt import hp +from functools import reduce +from typing import Dict, List + + +class_name = 'DefaultStrategy' + + +class DefaultStrategy(IStrategy): + """ + Default Strategy provided by freqtrade bot. + You can override it with your own strategy + """ + + # Minimal ROI designed for the strategy + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + stoploss = -0.10 + + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # Awesome oscillator + dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # Minus Directional Indicator / Movement + dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Overlap Studies + # ------------------------------------ + + # Previous Bollinger bands + # Because ta.BBANDS implementation is broken with small numbers, it actually + # returns middle band for all the three bands. Switch to qtpylib.bollinger_bands + # and use middle band instead. + dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband'] + """ + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + """ + + # EMA - Exponential Moving Average + dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # SAR Parabol + dataframe['sar'] = ta.SAR(dataframe) + + # SMA - Simple Moving Average + dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['rsi'] < 35) & + (dataframe['fastd'] < 35) & + (dataframe['adx'] > 30) & + (dataframe['plus_di'] > 0.5) + ) | + ( + (dataframe['adx'] > 65) & + (dataframe['plus_di'] > 0.5) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ( + (qtpylib.crossed_above(dataframe['rsi'], 70)) | + (qtpylib.crossed_above(dataframe['fastd'], 70)) + ) & + (dataframe['adx'] > 10) & + (dataframe['minus_di'] > 0) + ) | + ( + (dataframe['adx'] > 70) & + (dataframe['minus_di'] > 0.5) + ), + 'sell'] = 1 + return dataframe + + def hyperopt_space(self) -> List[Dict]: + """ + Define your Hyperopt space for the strategy + """ + space = { + 'mfi': hp.choice('mfi', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)} + ]), + 'fastd': hp.choice('fastd', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)} + ]), + 'adx': hp.choice('adx', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)} + ]), + 'rsi': hp.choice('rsi', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)} + ]), + 'uptrend_long_ema': hp.choice('uptrend_long_ema', [ + {'enabled': False}, + {'enabled': True} + ]), + 'uptrend_short_ema': hp.choice('uptrend_short_ema', [ + {'enabled': False}, + {'enabled': True} + ]), + 'over_sar': hp.choice('over_sar', [ + {'enabled': False}, + {'enabled': True} + ]), + 'green_candle': hp.choice('green_candle', [ + {'enabled': False}, + {'enabled': True} + ]), + 'uptrend_sma': hp.choice('uptrend_sma', [ + {'enabled': False}, + {'enabled': True} + ]), + 'trigger': hp.choice('trigger', [ + {'type': 'lower_bb'}, + {'type': 'faststoch10'}, + {'type': 'ao_cross_zero'}, + {'type': 'ema5_cross_ema10'}, + {'type': 'macd_cross_signal'}, + {'type': 'sar_reversal'}, + {'type': 'stochf_cross'}, + {'type': 'ht_sine'}, + ]), + 'stoploss': hp.uniform('stoploss', -0.5, -0.02), + } + return space + + def buy_strategy_generator(self, params) -> None: + """ + Define the buy strategy parameters to be used by hyperopt + """ + def populate_buy_trend(dataframe: DataFrame) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if params['uptrend_long_ema']['enabled']: + conditions.append(dataframe['ema50'] > dataframe['ema100']) + if params['uptrend_short_ema']['enabled']: + conditions.append(dataframe['ema5'] > dataframe['ema10']) + if params['mfi']['enabled']: + conditions.append(dataframe['mfi'] < params['mfi']['value']) + if params['fastd']['enabled']: + conditions.append(dataframe['fastd'] < params['fastd']['value']) + if params['adx']['enabled']: + conditions.append(dataframe['adx'] > params['adx']['value']) + if params['rsi']['enabled']: + conditions.append(dataframe['rsi'] < params['rsi']['value']) + if params['over_sar']['enabled']: + conditions.append(dataframe['close'] > dataframe['sar']) + if params['green_candle']['enabled']: + conditions.append(dataframe['close'] > dataframe['open']) + if params['uptrend_sma']['enabled']: + prevsma = dataframe['sma'].shift(1) + conditions.append(dataframe['sma'] > prevsma) + + # TRIGGERS + triggers = { + 'lower_bb': dataframe['tema'] <= dataframe['blower'], + 'faststoch10': (qtpylib.crossed_above(dataframe['fastd'], 10.0)), + 'ao_cross_zero': (qtpylib.crossed_above(dataframe['ao'], 0.0)), + 'ema5_cross_ema10': ( + qtpylib.crossed_above(dataframe['ema5'], dataframe['ema10']) + ), + 'macd_cross_signal': ( + qtpylib.crossed_above(dataframe['macd'], dataframe['macdsignal']) + ), + 'sar_reversal': (qtpylib.crossed_above(dataframe['close'], dataframe['sar'])), + 'stochf_cross': (qtpylib.crossed_above(dataframe['fastk'], dataframe['fastd'])), + 'ht_sine': (qtpylib.crossed_above(dataframe['htleadsine'], dataframe['htsine'])), + } + conditions.append(triggers.get(params['trigger']['type'])) + + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py new file mode 100644 index 000000000..4870b14db --- /dev/null +++ b/freqtrade/strategy/interface.py @@ -0,0 +1,56 @@ +from abc import ABC, abstractmethod +from pandas import DataFrame +from typing import Dict + + +class IStrategy(ABC): + @property + def name(self) -> str: + """ + Name of the strategy. + :return: str representation of the class name + """ + return self.__class__.__name__ + + """ + Attributes you can use: + minimal_roi -> Dict: Minimal ROI designed for the strategy + stoploss -> float: ptimal stoploss designed for the strategy + """ + + @abstractmethod + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + """ + Populate indicators that will be used in the Buy and Sell strategy + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :return: a Dataframe with all mandatory indicators for the strategies + """ + + @abstractmethod + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + :return: + """ + + @abstractmethod + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + + @abstractmethod + def hyperopt_space(self) -> Dict: + """ + Define your Hyperopt space for the strategy + """ + + @abstractmethod + def buy_strategy_generator(self, params) -> None: + """ + Define the buy strategy parameters to be used by hyperopt + """ diff --git a/freqtrade/strategy/strategy.py b/freqtrade/strategy/strategy.py new file mode 100644 index 000000000..19db069ea --- /dev/null +++ b/freqtrade/strategy/strategy.py @@ -0,0 +1,165 @@ +import os +import sys +import logging +import importlib + +from pandas import DataFrame +from typing import Dict +from freqtrade.strategy.interface import IStrategy + + +sys.path.insert(0, r'../../user_data/strategies') + + +class Strategy(object): + __instance = None + + DEFAULT_STRATEGY = 'default_strategy' + + def __new__(cls): + if Strategy.__instance is None: + Strategy.__instance = object.__new__(cls) + return Strategy.__instance + + def init(self, config): + self.logger = logging.getLogger(__name__) + + # Verify the strategy is in the configuration, otherwise fallback to the default strategy + if 'strategy' in config: + strategy = config['strategy'] + else: + strategy = self.DEFAULT_STRATEGY + + # Load the strategy + self._load_strategy(strategy) + + # Set attributes + # Check if we need to override configuration + if 'minimal_roi' in config: + self.custom_strategy.minimal_roi = config['minimal_roi'] + self.logger.info("Override strategy \'minimal_roi\' with value in config file.") + + if 'stoploss' in config: + self.custom_strategy.stoploss = config['stoploss'] + self.logger.info("Override strategy \'stoploss\' with value in config file.") + + self.minimal_roi = self.custom_strategy.minimal_roi + self.stoploss = self.custom_strategy.stoploss + + def _load_strategy(self, strategy_name: str) -> None: + """ + Search and load the custom strategy. If no strategy found, fallback on the default strategy + Set the object into self.custom_strategy + :param strategy_name: name of the module to import + :return: None + """ + + try: + # Start by sanitizing the file name (remove any extensions) + strategy_name = self._sanitize_module_name(filename=strategy_name) + + # Search where can be the strategy file + path = self._search_strategy(filename=strategy_name) + + # Load the strategy + self.custom_strategy = self._load_class(path + strategy_name) + + # Fallback to the default strategy + except (ImportError, TypeError): + self.custom_strategy = self._load_class('.' + self.DEFAULT_STRATEGY) + + def _load_class(self, filename: str) -> IStrategy: + """ + Import a strategy as a module + :param filename: path to the strategy (path from freqtrade/strategy/) + :return: return the strategy class + """ + module = importlib.import_module(filename, __package__) + custom_strategy = getattr(module, module.class_name) + + self.logger.info("Load strategy class: {} ({}.py)".format(module.class_name, filename)) + return custom_strategy() + + @staticmethod + def _sanitize_module_name(filename: str) -> str: + """ + Remove any extension from filename + :param filename: filename to sanatize + :return: return the filename without extensions + """ + filename = os.path.basename(filename) + filename = os.path.splitext(filename)[0] + return filename + + @staticmethod + def _search_strategy(filename: str) -> str: + """ + Search for the Strategy file in different folder + 1. search into the user_data/strategies folder + 2. search into the freqtrade/strategy folder + 3. if nothing found, return None + :param strategy_name: module name to search + :return: module path where is the strategy + """ + pwd = os.path.dirname(os.path.realpath(__file__)) + '/' + user_data = os.path.join(pwd, '..', '..', 'user_data', 'strategies', filename + '.py') + strategy_folder = os.path.join(pwd, filename + '.py') + + path = None + if os.path.isfile(user_data): + path = 'user_data.strategies.' + elif os.path.isfile(strategy_folder): + path = '.' + + return path + + def minimal_roi(self) -> Dict: + """ + Minimal ROI designed for the strategy + :return: Dict: Value for the Minimal ROI + """ + return + + def stoploss(self) -> float: + """ + Optimal stoploss designed for the strategy + :return: float | return None to disable it + """ + return self.custom_strategy.stoploss + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + """ + Populate indicators that will be used in the Buy and Sell strategy + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :return: a Dataframe with all mandatory indicators for the strategies + """ + return self.custom_strategy.populate_indicators(dataframe) + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + :return: + """ + return self.custom_strategy.populate_buy_trend(dataframe) + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + return self.custom_strategy.populate_sell_trend(dataframe) + + def hyperopt_space(self) -> Dict: + """ + Define your Hyperopt space for the strategy + """ + return self.custom_strategy.hyperopt_space() + + def buy_strategy_generator(self, params) -> None: + """ + Define the buy strategy parameters to be used by hyperopt + """ + return self.custom_strategy.buy_strategy_generator(params) diff --git a/freqtrade/tests/strategy/test_default_strategy.py b/freqtrade/tests/strategy/test_default_strategy.py new file mode 100644 index 000000000..cbf2bee2c --- /dev/null +++ b/freqtrade/tests/strategy/test_default_strategy.py @@ -0,0 +1,36 @@ +import json +import pytest +from pandas import DataFrame +from freqtrade.strategy.default_strategy import DefaultStrategy, class_name +from freqtrade.analyze import parse_ticker_dataframe + + +@pytest.fixture +def result(): + with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: + return parse_ticker_dataframe(json.load(data_file)) + + +def test_default_strategy_class_name(): + assert class_name == DefaultStrategy.__name__ + +def test_default_strategy_structure(): + assert hasattr(DefaultStrategy, 'minimal_roi') + assert hasattr(DefaultStrategy, 'stoploss') + assert hasattr(DefaultStrategy, 'populate_indicators') + assert hasattr(DefaultStrategy, 'populate_buy_trend') + assert hasattr(DefaultStrategy, 'populate_sell_trend') + assert hasattr(DefaultStrategy, 'hyperopt_space') + assert hasattr(DefaultStrategy, 'buy_strategy_generator') + +def test_default_strategy(result): + strategy = DefaultStrategy() + + assert type(strategy.minimal_roi) is dict + assert type(strategy.stoploss) is float + indicators = strategy.populate_indicators(result) + assert type(indicators) is DataFrame + assert type(strategy.populate_buy_trend(indicators)) is DataFrame + assert type(strategy.populate_sell_trend(indicators)) is DataFrame + assert type(strategy.hyperopt_space()) is dict + assert callable(strategy.buy_strategy_generator({})) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py new file mode 100644 index 000000000..b9e20d397 --- /dev/null +++ b/freqtrade/tests/strategy/test_strategy.py @@ -0,0 +1,132 @@ +import json +import logging +import pytest +from freqtrade.strategy.strategy import Strategy +from freqtrade.analyze import parse_ticker_dataframe + + +@pytest.fixture +def result(): + with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: + return parse_ticker_dataframe(json.load(data_file)) + + +def test_sanitize_module_name(): + assert Strategy._sanitize_module_name('default_strategy') == 'default_strategy' + assert Strategy._sanitize_module_name('default_strategy.py') == 'default_strategy' + assert Strategy._sanitize_module_name('../default_strategy.py') == 'default_strategy' + assert Strategy._sanitize_module_name('../default_strategy') == 'default_strategy' + assert Strategy._sanitize_module_name('.default_strategy') == '.default_strategy' + assert Strategy._sanitize_module_name('foo-bar') == 'foo-bar' + assert Strategy._sanitize_module_name('foo/bar') == 'bar' + + +def test_search_strategy(): + assert Strategy._search_strategy('default_strategy') == '.' + assert Strategy._search_strategy('super_duper') is None + + +def test_strategy_structure(): + assert hasattr(Strategy, 'init') + assert hasattr(Strategy, 'minimal_roi') + assert hasattr(Strategy, 'stoploss') + assert hasattr(Strategy, 'populate_indicators') + assert hasattr(Strategy, 'populate_buy_trend') + assert hasattr(Strategy, 'populate_sell_trend') + assert hasattr(Strategy, 'hyperopt_space') + assert hasattr(Strategy, 'buy_strategy_generator') + + +def test_load_strategy(result): + strategy = Strategy() + strategy.logger = logging.getLogger(__name__) + + assert not hasattr(Strategy, 'custom_strategy') + strategy._load_strategy('default_strategy') + + assert not hasattr(Strategy, 'custom_strategy') + + assert hasattr(strategy.custom_strategy, 'populate_indicators') + assert 'adx' in strategy.populate_indicators(result) + + +def test_strategy(result): + strategy = Strategy() + strategy.init({'strategy': 'default_strategy'}) + + assert hasattr(strategy.custom_strategy, 'minimal_roi') + assert strategy.minimal_roi['0'] == 0.04 + + assert hasattr(strategy.custom_strategy, 'stoploss') + assert strategy.stoploss == -0.10 + + assert hasattr(strategy.custom_strategy, 'populate_indicators') + assert 'adx' in strategy.populate_indicators(result) + + assert hasattr(strategy.custom_strategy, 'populate_buy_trend') + dataframe = strategy.populate_buy_trend(strategy.populate_indicators(result)) + assert 'buy' in dataframe.columns + + assert hasattr(strategy.custom_strategy, 'populate_sell_trend') + dataframe = strategy.populate_sell_trend(strategy.populate_indicators(result)) + assert 'sell' in dataframe.columns + + assert hasattr(strategy.custom_strategy, 'hyperopt_space') + assert 'adx' in strategy.hyperopt_space() + + assert hasattr(strategy.custom_strategy, 'buy_strategy_generator') + assert callable(strategy.buy_strategy_generator({})) + + +def test_strategy_override_minimal_roi(caplog): + config = { + 'strategy': 'default_strategy', + 'minimal_roi': { + "0": 0.5 + } + } + strategy = Strategy() + strategy.init(config) + + assert hasattr(strategy.custom_strategy, 'minimal_roi') + assert strategy.minimal_roi['0'] == 0.5 + assert ('freqtrade.strategy.strategy', + logging.INFO, + 'Override strategy \'minimal_roi\' with value in config file.' + ) in caplog.record_tuples + + +def test_strategy_override_stoploss(caplog): + config = { + 'strategy': 'default_strategy', + 'stoploss': -0.5 + } + strategy = Strategy() + strategy.init(config) + + assert hasattr(strategy.custom_strategy, 'stoploss') + assert strategy.stoploss == -0.5 + assert ('freqtrade.strategy.strategy', + logging.INFO, + 'Override strategy \'stoploss\' with value in config file.' + ) in caplog.record_tuples + + +def test_strategy_fallback_default_strategy(): + strategy = Strategy() + strategy.logger = logging.getLogger(__name__) + + assert not hasattr(Strategy, 'custom_strategy') + strategy._load_strategy('../../super_duper') + assert not hasattr(Strategy, 'custom_strategy') + +def test_strategy_singleton(): + strategy1 = Strategy() + strategy1.init({'strategy': 'default_strategy'}) + + assert hasattr(strategy1.custom_strategy, 'minimal_roi') + assert strategy1.minimal_roi['0'] == 0.04 + + strategy2 = Strategy() + assert hasattr(strategy2.custom_strategy, 'minimal_roi') + assert strategy2.minimal_roi['0'] == 0.04 diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 4c8378b3e..472b5eff5 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -9,6 +9,7 @@ from pandas import DataFrame from freqtrade.analyze import (get_signal, parse_ticker_dataframe, populate_buy_trend, populate_indicators, populate_sell_trend) +from freqtrade.strategy.strategy import Strategy @pytest.fixture @@ -27,11 +28,17 @@ def test_dataframe_correct_length(result): def test_populates_buy_trend(result): + # Load the default strategy for the unit test, because this logic is done in main.py + Strategy().init({'strategy': 'default_strategy'}) + dataframe = populate_buy_trend(populate_indicators(result)) assert 'buy' in dataframe.columns def test_populates_sell_trend(result): + # Load the default strategy for the unit test, because this logic is done in main.py + Strategy().init({'strategy': 'default_strategy'}) + dataframe = populate_sell_trend(populate_indicators(result)) assert 'sell' in dataframe.columns diff --git a/user_data/data/.gitkeep b/user_data/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/user_data/strategies/__init__.py b/user_data/strategies/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py new file mode 100644 index 000000000..728cc6328 --- /dev/null +++ b/user_data/strategies/test_strategy.py @@ -0,0 +1,129 @@ + +# --- Do not remove these libs --- +from freqtrade.strategy.interface import IStrategy +from typing import Dict, List +from hyperopt import hp +from functools import reduce +from pandas import DataFrame +# -------------------------------- + +# Add your lib to import here +import talib.abstract as ta + + +# Update this variable if you change the class name +class_name = 'TestStrategy' + + +class TestStrategy(IStrategy): + """ + This is a test strategy to inspire you. + You can: + - Rename the class name (Do not forget to update class_name) + - Add any methods you want to build your strategy + - Add any lib you need to build your strategy + + You must keep: + - the lib in the section "Do not remove these libs" + - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, + populate_sell_trend, hyperopt_space, buy_strategy_generator + """ + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.10 + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + """ + + dataframe['adx'] = ta.ADX(dataframe) + dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband'] + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['adx'] > 30) & + (dataframe['tema'] <= dataframe['blower']) & + (dataframe['tema'] > dataframe['tema'].shift(1)) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['adx'] > 70) & + (dataframe['tema'] > dataframe['blower']) & + (dataframe['tema'] < dataframe['tema'].shift(1)) + ), + 'sell'] = 1 + return dataframe + + def hyperopt_space(self) -> List[Dict]: + """ + Define your Hyperopt space for the strategy + """ + space = { + 'adx': hp.choice('adx', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)} + ]), + 'trigger': hp.choice('trigger', [ + {'type': 'lower_bb'}, + ]), + 'stoploss': hp.uniform('stoploss', -0.5, -0.02), + } + return space + + def buy_strategy_generator(self, params) -> None: + """ + Define the buy strategy parameters to be used by hyperopt + """ + def populate_buy_trend(dataframe: DataFrame) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if params['adx']['enabled']: + conditions.append(dataframe['adx'] > params['adx']['value']) + + # TRIGGERS + triggers = { + 'lower_bb': dataframe['tema'] <= dataframe['blower'], + } + conditions.append(triggers.get(params['trigger']['type'])) + + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend