diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 212a3f56c..6cf0d3f90 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -204,19 +204,39 @@ signal. Given following result from hyperopt: ``` Best parameters: { - "adx": 1, - "adx-value": 15.0, - "fastd": 1, - "fastd-value": 40.0, - "green_candle": 1, - "mfi": 0, - "over_sar": 0, - "rsi": 1, - "rsi-value": 37.0, - "trigger": 0, - "uptrend_long_ema": 1, - "uptrend_short_ema": 0, - "uptrend_sma": 0 + "adx": { + "enabled": true, + "value": 15.0 + }, + "fastd": { + "enabled": true, + "value": 40.0 + }, + "green_candle": { + "enabled": true + }, + "mfi": { + "enabled": false + }, + "over_sar": { + "enabled": false + }, + "rsi": { + "enabled": true, + "value": 37.0 + }, + "trigger": { + "type": "lower_bb" + }, + "uptrend_long_ema": { + "enabled": true + }, + "uptrend_short_ema": { + "enabled": false + }, + "uptrend_sma": { + "enabled": false + } } Best Result: @@ -224,14 +244,14 @@ Best Result: ``` You should understand this result like: -- You should **consider** the guard "adx" (`"adx": 1,` = `adx` is true) -and the best value is `15.0` (`"adx-value": 15.0,`) -- You should **consider** the guard "fastd" (`"fastd": 1,` = `fastd` -is true) and the best value is `40.0` (`"fastd-value": 40.0,`) +- You should **consider** the guard "adx" (`"adx"` is `"enabled": true`) +and the best value is `15.0` (`"value": 15.0,`) +- You should **consider** the guard "fastd" (`"fastd"` is `"enabled": +true`) and the best value is `40.0` (`"value": 40.0,`) - You should **consider** to enable the guard "green_candle" -(`"green_candle": 1,` = `candle` is true) but this guards as no +(`"green_candle"` is `"enabled": true`) but this guards as no customizable value. -- You should **ignore** the guard "mfi" (`"mfi": 0,` = `mfi` is false) +- You should **ignore** the guard "mfi" (`"mfi"` is `"enabled": false`) - and so on... @@ -239,8 +259,8 @@ You have to look from [freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L170-L200) what those values match to. -So for example you had `adx-value: 15.0` (and `adx: 1` was true) so we -would look at `adx`-block from +So for example you had `adx:` with the `value: 15.0` so we would look +at `adx`-block from [freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L178-L179). That translates to the following code block to [analyze.populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 91a041d52..4a3c0771e 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -11,7 +11,7 @@ import talib.abstract as ta from pandas import DataFrame, to_datetime from freqtrade.exchange import get_ticker_history -from freqtrade.vendor.qtpylib.indicators import awesome_oscillator, crossed_above +from freqtrade.vendor.qtpylib.indicators import awesome_oscillator, PandasObject as qtpylib logger = logging.getLogger(__name__) @@ -40,34 +40,185 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame: def populate_indicators(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['sar'] = ta.SAR(dataframe) + + # Momentum Indicator + # ------------------------------------ + + # ADX dataframe['adx'] = ta.ADX(dataframe) - stoch = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch['fastd'] - dataframe['fastk'] = stoch['fastk'] - dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband'] - dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) - dataframe['mfi'] = ta.MFI(dataframe) - dataframe['rsi'] = ta.RSI(dataframe) - dataframe['cci'] = ta.CCI(dataframe) - 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) + + # Awesome oscillator dataframe['ao'] = 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) + """ + # 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['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'] - dataframe['plus_dm'] = ta.PLUS_DM(dataframe) - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - dataframe['minus_dm'] = ta.MINUS_DM(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # 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 @@ -102,8 +253,8 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame: dataframe.loc[ ( ( - (crossed_above(dataframe['rsi'], 70)) | - (crossed_above(dataframe['fastd'], 70)) + (qtpylib.crossed_above(dataframe['rsi'], 70)) | + (qtpylib.crossed_above(dataframe['fastd'], 70)) ) & (dataframe['adx'] > 10) & (dataframe['minus_di'] > 0) diff --git a/freqtrade/fiat_convert.py b/freqtrade/fiat_convert.py index 567835eed..3f0b6330b 100644 --- a/freqtrade/fiat_convert.py +++ b/freqtrade/fiat_convert.py @@ -57,7 +57,11 @@ class CryptoToFiatConverter(): ] def __init__(self) -> None: - self._coinmarketcap = Pymarketcap() + try: + self._coinmarketcap = Pymarketcap() + except BaseException: + self._coinmarketcap = None + self._pairs = [] def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: @@ -147,10 +151,12 @@ class CryptoToFiatConverter(): # Check if the fiat convertion you want is supported if not self._is_supported_fiat(fiat=fiat_symbol): raise ValueError('The fiat {} is not supported.'.format(fiat_symbol)) - - return float( - self._coinmarketcap.ticker( - currency=crypto_symbol, - convert=fiat_symbol - )['price_' + fiat_symbol.lower()] - ) + try: + return float( + self._coinmarketcap.ticker( + currency=crypto_symbol, + convert=fiat_symbol + )['price_' + fiat_symbol.lower()] + ) + except BaseException: + return 0.0 diff --git a/freqtrade/main.py b/freqtrade/main.py index 254bed33e..5e8680b85 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -247,12 +247,6 @@ def handle_trade(trade: Trade) -> bool: logger.debug('Handling %s ...', trade) current_rate = exchange.get_ticker(trade.pair)['bid'] - # Experimental: Check if the trade is profitable before selling it (avoid selling at loss) - if _CONF.get('experimental', {}).get('sell_profit_only'): - logger.debug('Checking if trade is profitable ...') - if trade.calc_profit(rate=current_rate) <= 0: - return False - # Check if minimal roi has been reached if min_roi_reached(trade, current_rate, datetime.utcnow()): logger.debug('Executing sell due to ROI ...') @@ -261,6 +255,11 @@ def handle_trade(trade: Trade) -> bool: # Experimental: Check if sell signal has been enabled and triggered if _CONF.get('experimental', {}).get('use_sell_signal'): + # Experimental: Check if the trade is profitable before selling it (avoid selling at loss) + if _CONF.get('experimental', {}).get('sell_profit_only'): + logger.debug('Checking if trade is profitable ...') + if trade.calc_profit(rate=current_rate) <= 0: + return False logger.debug('Checking sell_signal ...') if get_signal(trade.pair, SignalType.SELL): logger.debug('Executing sell due to sell signal ...') @@ -399,14 +398,18 @@ def cleanup() -> None: exit(0) -def main() -> None: +def main(sysargv=sys.argv[1:]) -> None: """ Loads and validates the config and handles the main loop :return: None """ global _CONF - args = parse_args(sys.argv[1:]) - if not args: + args = parse_args(sysargv, + 'Simple High Frequency Trading Bot for crypto currencies') + + # A subcommand has been issued + if hasattr(args, 'func'): + args.func(args) exit(0) # Initialize logger diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 900aa8763..e62bea8bc 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -81,21 +81,12 @@ def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: return result -def parse_args(args: List[str]): +def parse_args_common(args: List[str], description: str): """ - Parses given arguments and returns an argparse Namespace instance. - Returns None if a sub command has been selected and executed. + Parses given common arguments and returns them as a parsed object. """ parser = argparse.ArgumentParser( - description='Simple High Frequency Trading Bot for crypto currencies' - ) - parser.add_argument( - '-c', '--config', - help='specify configuration file (default: config.json)', - dest='config', - default='config.json', - type=str, - metavar='PATH', + description=description ) parser.add_argument( '-v', '--verbose', @@ -110,6 +101,30 @@ def parse_args(args: List[str]): action='version', version='%(prog)s {}'.format(__version__), ) + parser.add_argument( + '-c', '--config', + help='specify configuration file (default: config.json)', + dest='config', + default='config.json', + type=str, + metavar='PATH', + ) + return parser + + +def parse_args(args: List[str], description: str): + """ + Parses given arguments and returns an argparse Namespace instance. + Returns None if a sub command has been selected and executed. + """ + parser = parse_args_common(args, description) + parser.add_argument( + '--dry-run-db', + help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" instead of memory DB. Work only if dry_run is \ + enabled.', # noqa + action='store_true', + dest='dry_run_db', + ) parser.add_argument( '--dynamic-whitelist', help='dynamically generate and update whitelist based on 24h BaseVolume (Default 20 currencies)', # noqa @@ -119,22 +134,9 @@ def parse_args(args: List[str]): metavar='INT', nargs='?', ) - parser.add_argument( - '--dry-run-db', - help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" instead of memory DB. Work only if dry_run is \ - enabled.', # noqa - action='store_true', - dest='dry_run_db', - ) + build_subcommands(parser) - parsed_args = parser.parse_args(args) - - # No subcommand as been selected - if not hasattr(parsed_args, 'func'): - return parsed_args - - parsed_args.func(parsed_args) - return None + return parser.parse_args(args) def build_subcommands(parser: argparse.ArgumentParser) -> None: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d0d0916f8..88d61e37b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -8,7 +8,7 @@ from functools import reduce from math import exp from operator import itemgetter -from hyperopt import fmin, tpe, hp, Trials, STATUS_OK, STATUS_FAIL +from hyperopt import fmin, tpe, hp, Trials, STATUS_OK, STATUS_FAIL, space_eval from hyperopt.mongoexp import MongoTrials from pandas import DataFrame @@ -209,7 +209,7 @@ def buy_strategy_generator(params): def start(args): - global TOTAL_TRIES, PROCESSED + global TOTAL_TRIES, PROCESSED, SPACE TOTAL_TRIES = args.epochs exchange._API = Bittrex({'key': '', 'secret': ''}) @@ -236,6 +236,11 @@ def start(args): trials = Trials() best = fmin(fn=optimizer, space=SPACE, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials) + + # Improve best parameter logging display + if best: + best = space_eval(SPACE, best) + logger.info('Best parameters:\n%s', json.dumps(best, indent=4)) results = sorted(trials.results, key=itemgetter('loss')) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 104b3bfdd..24d3ec8c6 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -77,3 +77,40 @@ def test_no_log_if_loss_does_not_improve(mocker): }) assert not logger.called + + +def test_fmin_best_results(mocker, caplog): + fmin_result = { + "adx": 1, + "adx-value": 15.0, + "fastd": 1, + "fastd-value": 40.0, + "green_candle": 1, + "mfi": 0, + "over_sar": 0, + "rsi": 1, + "rsi-value": 37.0, + "trigger": 2, + "uptrend_long_ema": 1, + "uptrend_short_ema": 0, + "uptrend_sma": 0 + } + + mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker)) + mocker.patch('freqtrade.optimize.preprocess') + mocker.patch('freqtrade.optimize.load_data') + mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result) + + args = mocker.Mock(epochs=1, config='config.json.example') + start(args) + + exists = [ + 'Best parameters', + '"adx": {\n "enabled": true,\n "value": 15.0\n },', + '"green_candle": {\n "enabled": true\n },', + '"mfi": {\n "enabled": false\n },', + '"trigger": {\n "type": "ao_cross_zero"\n },' + ] + + for line in exists: + assert line in caplog.text diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index eca65bd5a..428c544c1 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -167,6 +167,7 @@ def test_profit_handle( mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', ticker=MagicMock(return_value={'price_usd': 15000.0}), _cache_symbols=MagicMock(return_value={'BTC': 1})) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) init(default_conf, create_engine('sqlite://')) _profit(bot=MagicMock(), update=update) @@ -422,6 +423,7 @@ def test_daily_handle( mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', ticker=MagicMock(return_value={'price_usd': 15000.0}), _cache_symbols=MagicMock(return_value={'BTC': 1})) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) init(default_conf, create_engine('sqlite://')) # Create some test data diff --git a/freqtrade/tests/test_fiat_convert.py b/freqtrade/tests/test_fiat_convert.py index 13597f3a3..7ad316042 100644 --- a/freqtrade/tests/test_fiat_convert.py +++ b/freqtrade/tests/test_fiat_convert.py @@ -72,8 +72,11 @@ def test_fiat_convert_find_price(mocker): with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'): fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC') + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=12345.0) assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0 assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0 + + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=13000.2) assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2 @@ -83,6 +86,7 @@ def test_fiat_convert_get_price(mocker): 'price_eur': 15000.0 }) mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0) fiat_convert = CryptoToFiatConverter() @@ -109,3 +113,12 @@ def test_fiat_convert_get_price(mocker): fiat_convert._pairs[0]._expiration = expiration assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0 assert fiat_convert._pairs[0]._expiration is not expiration + + +def test_fiat_convert_without_network(mocker): + Pymarketcap = MagicMock(side_effect=ImportError('Oh boy, you have no network!')) + mocker.patch('freqtrade.fiat_convert.Pymarketcap', Pymarketcap) + + fiat_convert = CryptoToFiatConverter() + assert fiat_convert._coinmarketcap is None + assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0 diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 004126268..cceb555f7 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -15,6 +15,39 @@ from freqtrade.main import create_trade, handle_trade, init, \ get_target_bid, _process, execute_sell, check_handle_timedout from freqtrade.misc import get_state, State from freqtrade.persistence import Trade +import freqtrade.main as main + + +# Test that main() can start backtesting or hyperopt. +# and also ensure we can pass some specific arguments +# argument parsing is done in test_misc.py + +def test_parse_args_backtesting(mocker): + backtesting_mock = mocker.patch( + 'freqtrade.optimize.backtesting.start', MagicMock()) + with pytest.raises(SystemExit, match=r'0'): + main.main(['backtesting']) + assert backtesting_mock.call_count == 1 + call_args = backtesting_mock.call_args[0][0] + assert call_args.config == 'config.json' + assert call_args.live is False + assert call_args.loglevel == 20 + assert call_args.subparser == 'backtesting' + assert call_args.func is not None + assert call_args.ticker_interval == 5 + + +def test_main_start_hyperopt(mocker): + hyperopt_mock = mocker.patch( + 'freqtrade.optimize.hyperopt.start', MagicMock()) + with pytest.raises(SystemExit, match=r'0'): + main.main(['hyperopt']) + assert hyperopt_mock.call_count == 1 + call_args = hyperopt_mock.call_args[0][0] + assert call_args.config == 'config.json' + assert call_args.loglevel == 20 + assert call_args.subparser == 'hyperopt' + assert call_args.func is not None def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker): @@ -331,7 +364,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, health, limit_buy_order cancel_order=cancel_order_mock) init(default_conf, create_engine('sqlite://')) - tradeBuy = Trade( + trade_buy = Trade( pair='BTC_ETH', open_rate=0.00001099, exchange='BITTREX', @@ -343,12 +376,12 @@ def test_check_handle_timedout_buy(default_conf, ticker, health, limit_buy_order is_open=True ) - Trade.session.add(tradeBuy) + Trade.session.add(trade_buy) # check it does cancel buy orders over the time limit check_handle_timedout(600) assert cancel_order_mock.call_count == 1 - trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all() + trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() assert len(trades) == 0 @@ -363,7 +396,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, health, limit_sell_ord cancel_order=cancel_order_mock) init(default_conf, create_engine('sqlite://')) - tradeSell = Trade( + trade_sell = Trade( pair='BTC_ETH', open_rate=0.00001099, exchange='BITTREX', @@ -376,12 +409,12 @@ def test_check_handle_timedout_sell(default_conf, ticker, health, limit_sell_ord is_open=False ) - Trade.session.add(tradeSell) + Trade.session.add(trade_sell) # check it does cancel sell orders over the time limit check_handle_timedout(600) assert cancel_order_mock.call_count == 1 - assert tradeSell.is_open is True + assert trade_sell.is_open is True def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, @@ -396,7 +429,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old cancel_order=cancel_order_mock) init(default_conf, create_engine('sqlite://')) - tradeBuy = Trade( + trade_buy = Trade( pair='BTC_ETH', open_rate=0.00001099, exchange='BITTREX', @@ -408,16 +441,16 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old is_open=True ) - Trade.session.add(tradeBuy) + Trade.session.add(trade_buy) # check it does cancel buy orders over the time limit # note this is for a partially-complete buy order check_handle_timedout(600) assert cancel_order_mock.call_count == 1 - trades = Trade.query.filter(Trade.open_order_id.is_(tradeBuy.open_order_id)).all() + trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() assert len(trades) == 1 assert trades[0].amount == 23.0 - assert trades[0].stake_amount == tradeBuy.open_rate * trades[0].amount + assert trades[0].stake_amount == trade_buy.open_rate * trades[0].amount def test_balance_fully_ask_side(mocker): @@ -537,10 +570,13 @@ def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker) def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker): - default_conf['experimental'] = {} - default_conf['experimental']['sell_profit_only'] = True + default_conf['experimental'] = { + 'use_sell_signal': True, + 'sell_profit_only': True, + } mocker.patch.dict('freqtrade.main._CONF', default_conf) + mocker.patch('freqtrade.main.min_roi_reached', return_value=False) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', @@ -561,10 +597,13 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker): def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker): - default_conf['experimental'] = {} - default_conf['experimental']['sell_profit_only'] = False + default_conf['experimental'] = { + 'use_sell_signal': True, + 'sell_profit_only': False, + } mocker.patch.dict('freqtrade.main._CONF', default_conf) + mocker.patch('freqtrade.main.min_roi_reached', return_value=False) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', @@ -585,10 +624,13 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker): def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker): - default_conf['experimental'] = {} - default_conf['experimental']['sell_profit_only'] = True + default_conf['experimental'] = { + 'use_sell_signal': True, + 'sell_profit_only': True, + } mocker.patch.dict('freqtrade.main._CONF', default_conf) + mocker.patch('freqtrade.main.min_roi_reached', return_value=False) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', @@ -609,10 +651,13 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker): def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker): - default_conf['experimental'] = {} - default_conf['experimental']['sell_profit_only'] = False + default_conf['experimental'] = { + 'use_sell_signal': True, + 'sell_profit_only': False, + } mocker.patch.dict('freqtrade.main._CONF', default_conf) + mocker.patch('freqtrade.main.min_roi_reached', return_value=False) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index cd529039a..0b85000cb 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -1,13 +1,14 @@ # pragma pylint: disable=missing-docstring,C0103 import json import time +import argparse from copy import deepcopy -from unittest.mock import MagicMock import pytest from jsonschema import ValidationError -from freqtrade.misc import throttle, parse_args, load_config +from freqtrade.misc import throttle, parse_args, load_config,\ + parse_args_common def test_throttle(): @@ -38,89 +39,83 @@ def test_throttle_with_assets(): assert result == -1 +# Parse common command-line-arguments +# used for all tools + + +def test_parse_args_none(): + args = parse_args_common([], '') + assert isinstance(args, argparse.ArgumentParser) + + def test_parse_args_defaults(): - args = parse_args([]) - assert args is not None + args = parse_args([], '') assert args.config == 'config.json' assert args.dynamic_whitelist is None assert args.loglevel == 20 -def test_parse_args_invalid(): - with pytest.raises(SystemExit, match=r'2'): - parse_args(['-c']) - - def test_parse_args_config(): - args = parse_args(['-c', '/dev/null']) - assert args is not None + args = parse_args(['-c', '/dev/null'], '') assert args.config == '/dev/null' - args = parse_args(['--config', '/dev/null']) - assert args is not None + args = parse_args(['--config', '/dev/null'], '') assert args.config == '/dev/null' def test_parse_args_verbose(): - args = parse_args(['-v']) - assert args is not None + args = parse_args(['-v'], '') + assert args.loglevel == 10 + + args = parse_args(['--verbose'], '') assert args.loglevel == 10 +def test_parse_args_version(): + with pytest.raises(SystemExit, match=r'0'): + parse_args(['--version'], '') + + +def test_parse_args_invalid(): + with pytest.raises(SystemExit, match=r'2'): + parse_args(['-c'], '') + + +# Parse command-line-arguments +# used for main, backtesting and hyperopt + + def test_parse_args_dynamic_whitelist(): - args = parse_args(['--dynamic-whitelist']) - assert args is not None + args = parse_args(['--dynamic-whitelist'], '') assert args.dynamic_whitelist is 20 def test_parse_args_dynamic_whitelist_10(): - args = parse_args(['--dynamic-whitelist', '10']) - assert args is not None + args = parse_args(['--dynamic-whitelist', '10'], '') assert args.dynamic_whitelist is 10 def test_parse_args_dynamic_whitelist_invalid_values(): with pytest.raises(SystemExit, match=r'2'): - parse_args(['--dynamic-whitelist', 'abc']) - - -def test_parse_args_backtesting(mocker): - backtesting_mock = mocker.patch( - 'freqtrade.optimize.backtesting.start', MagicMock()) - args = parse_args(['backtesting']) - assert args is None - assert backtesting_mock.call_count == 1 - - call_args = backtesting_mock.call_args[0][0] - assert call_args.config == 'config.json' - assert call_args.live is False - assert call_args.loglevel == 20 - assert call_args.subparser == 'backtesting' - assert call_args.func is not None - assert call_args.ticker_interval == 5 + parse_args(['--dynamic-whitelist', 'abc'], '') def test_parse_args_backtesting_invalid(): with pytest.raises(SystemExit, match=r'2'): - parse_args(['backtesting --ticker-interval']) + parse_args(['backtesting --ticker-interval'], '') with pytest.raises(SystemExit, match=r'2'): - parse_args(['backtesting --ticker-interval', 'abc']) + parse_args(['backtesting --ticker-interval', 'abc'], '') -def test_parse_args_backtesting_custom(mocker): - backtesting_mock = mocker.patch( - 'freqtrade.optimize.backtesting.start', MagicMock()) - args = parse_args([ +def test_parse_args_backtesting_custom(): + args = [ '-c', 'test_conf.json', 'backtesting', '--live', '--ticker-interval', '1', - '--refresh-pairs-cached']) - assert args is None - assert backtesting_mock.call_count == 1 - - call_args = backtesting_mock.call_args[0][0] + '--refresh-pairs-cached'] + call_args = parse_args(args, '') assert call_args.config == 'test_conf.json' assert call_args.live is True assert call_args.loglevel == 20 @@ -130,28 +125,9 @@ def test_parse_args_backtesting_custom(mocker): assert call_args.refresh_pairs is True -def test_parse_args_hyperopt(mocker): - hyperopt_mock = mocker.patch( - 'freqtrade.optimize.hyperopt.start', MagicMock()) - args = parse_args(['hyperopt']) - assert args is None - assert hyperopt_mock.call_count == 1 - - call_args = hyperopt_mock.call_args[0][0] - assert call_args.config == 'config.json' - assert call_args.loglevel == 20 - assert call_args.subparser == 'hyperopt' - assert call_args.func is not None - - def test_parse_args_hyperopt_custom(mocker): - hyperopt_mock = mocker.patch( - 'freqtrade.optimize.hyperopt.start', MagicMock()) - args = parse_args(['-c', 'test_conf.json', 'hyperopt', '--epochs', '20']) - assert args is None - assert hyperopt_mock.call_count == 1 - - call_args = hyperopt_mock.call_args[0][0] + args = ['-c', 'test_conf.json', 'hyperopt', '--epochs', '20'] + call_args = parse_args(args, '') assert call_args.config == 'test_conf.json' assert call_args.epochs == 20 assert call_args.loglevel == 20 diff --git a/requirements.txt b/requirements.txt index 04e164825..74a3e2b07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ pandas==0.22.0 scikit-learn==0.19.1 scipy==1.0.0 jsonschema==2.6.0 -numpy==1.13.3 +numpy==1.14.0 TA-Lib==0.4.10 pytest==3.3.2 pytest-mock==1.6.3 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 0d193726d..44961891c 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -1,17 +1,33 @@ #!/usr/bin/env python3 +import sys +import argparse import matplotlib # Install PYQT5 manually if you want to test this helper function matplotlib.use("Qt5Agg") import matplotlib.pyplot as plt from freqtrade import exchange, analyze +from freqtrade.misc import parse_args_common -def plot_analyzed_dataframe(pair: str) -> None: +def plot_parse_args(args ): + parser = parse_args_common(args, 'Graph utility') + parser.add_argument( + '-p', '--pair', + help = 'What currency pair', + dest = 'pair', + default = 'BTC_ETH', + type = str, + ) + return parser.parse_args(args) + + +def plot_analyzed_dataframe(args) -> None: """ Calls analyze() and plots the returned dataframe :param pair: pair as str :return: None """ + pair = args.pair # Init Bittrex to use public API exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) @@ -50,4 +66,5 @@ def plot_analyzed_dataframe(pair: str) -> None: if __name__ == '__main__': - plot_analyzed_dataframe('BTC_ETH') + args = plot_parse_args(sys.argv[1:]) + plot_analyzed_dataframe(args)