From a4d2bb6f294d2562ae9b90105fae9e486d0630d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Oct 2018 21:28:48 +0200 Subject: [PATCH 01/26] Fix "No market symbol" exception in telegram calls --- freqtrade/exchange/__init__.py | 2 ++ freqtrade/rpc/rpc.py | 21 +++++++++++++++------ freqtrade/tests/exchange/test_exchange.py | 4 ++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 7fd0e5f43..4af9db6db 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -375,6 +375,8 @@ class Exchange(object): def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: if refresh or pair not in self._cached_ticker.keys(): try: + if pair not in self._api.markets: + raise DependencyException(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) try: self._cached_ticker[pair] = { diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d653ea176..900ad1998 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -10,10 +10,10 @@ from typing import Dict, Any, List, Optional import arrow import sqlalchemy as sql -from numpy import mean, nan_to_num +from numpy import mean, nan_to_num, NAN from pandas import DataFrame -from freqtrade import TemporaryError +from freqtrade import TemporaryError, DependencyException from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.misc import shorten_date from freqtrade.persistence import Trade @@ -93,7 +93,10 @@ class RPC(object): if trade.open_order_id: order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) # calculate profit and send message to user - current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] + try: + current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] + except DependencyException: + current_rate = NAN current_profit = trade.calc_profit_percent(current_rate) fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' if trade.close_profit else None) @@ -122,7 +125,10 @@ class RPC(object): trades_list = [] for trade in trades: # calculate profit and send message to user - current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] + try: + current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] + except DependencyException: + current_rate = NAN trade_perc = (100 * trade.calc_profit_percent(current_rate)) trades_list.append([ trade.id, @@ -207,7 +213,10 @@ class RPC(object): profit_closed_percent.append(profit_percent) else: # Get current rate - current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] + try: + current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] + except DependencyException: + current_rate = NAN profit_percent = trade.calc_profit_percent(rate=current_rate) profit_all_coin.append( @@ -275,7 +284,7 @@ class RPC(object): rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid'] else: rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid'] - except TemporaryError: + except (TemporaryError, DependencyException): continue est_btc: float = rate * balance['total'] total = total + est_btc diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 93cd1e546..788ef4518 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -572,6 +572,7 @@ def test_get_ticker(default_conf, mocker): 'last': 0.0001, } api_mock.fetch_ticker = MagicMock(return_value=tick) + api_mock.markets = {'ETH/BTC': {}} exchange = get_patched_exchange(mocker, default_conf, api_mock) # retrieve original ticker ticker = exchange.get_ticker(pair='ETH/BTC') @@ -614,6 +615,9 @@ def test_get_ticker(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.get_ticker(pair='ETH/BTC', refresh=True) + with pytest.raises(DependencyException, match=r'Pair XRP/ETH not available'): + exchange.get_ticker(pair='XRP/ETH', refresh=True) + def test_get_history(default_conf, mocker, caplog): exchange = get_patched_exchange(mocker, default_conf) From 362865981060c44cdef6586ae28ce117f93a0031 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Oct 2018 21:50:59 +0200 Subject: [PATCH 02/26] Add tests to check if no failure occurs when pair is not available --- freqtrade/tests/rpc/test_rpc.py | 46 ++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 88bf5e9ad..14320cd54 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -5,8 +5,9 @@ from datetime import datetime from unittest.mock import MagicMock, ANY import pytest +from numpy import NAN, isnan -from freqtrade import TemporaryError +from freqtrade import TemporaryError, DependencyException from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade @@ -61,6 +62,27 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: 'open_order': '(limit buy rem=0.00000000)' } == results[0] + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) + # invalidate ticker cache + rpc._freqtrade.exchange._cached_ticker = {} + results = rpc._rpc_trade_status() + assert isnan(results[0]['current_profit']) + assert isnan(results[0]['current_rate']) + assert { + 'trade_id': 1, + 'pair': 'ETH/BTC', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'date': ANY, + 'open_rate': 1.099e-05, + 'close_rate': None, + 'current_rate': ANY, + 'amount': 90.99181074, + 'close_profit': None, + 'current_profit': ANY, + 'open_order': '(limit buy rem=0.00000000)' + } == results[0] + def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: patch_coinmarketcap(mocker) @@ -87,6 +109,15 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: assert 'ETH/BTC' in result['Pair'].all() assert '-0.59%' in result['Profit'].all() + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) + # invalidate ticker cache + rpc._freqtrade.exchange._cached_ticker = {} + result = rpc._rpc_status_table() + assert 'just now' in result['Since'].all() + assert 'ETH/BTC' in result['Pair'].all() + assert 'nan%' in result['Profit'].all() + def test_rpc_daily_profit(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: @@ -208,6 +239,19 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert stats['best_pair'] == 'ETH/BTC' assert prec_satoshi(stats['best_rate'], 6.2) + # Test non-available pair + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) + # invalidate ticker cache + rpc._freqtrade.exchange._cached_ticker = {} + stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) + assert stats['trade_count'] == 2 + assert stats['first_trade_date'] == 'just now' + assert stats['latest_trade_date'] == 'just now' + assert stats['avg_duration'] == '0:00:00' + assert stats['best_pair'] == 'ETH/BTC' + assert prec_satoshi(stats['best_rate'], 6.2) + assert isnan(stats['profit_all_coin']) # Test that rpc_trade_statistics can handle trades that lacks # trade.open_rate (it is set to None) From 138c8152c20831f836f1a82c759ec86ce49cb27f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Oct 2018 22:03:54 +0200 Subject: [PATCH 03/26] remove unused import --- freqtrade/tests/rpc/test_rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 14320cd54..b181231c8 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -5,7 +5,7 @@ from datetime import datetime from unittest.mock import MagicMock, ANY import pytest -from numpy import NAN, isnan +from numpy import isnan from freqtrade import TemporaryError, DependencyException from freqtrade.fiat_convert import CryptoToFiatConverter @@ -253,6 +253,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert prec_satoshi(stats['best_rate'], 6.2) assert isnan(stats['profit_all_coin']) + # Test that rpc_trade_statistics can handle trades that lacks # trade.open_rate (it is set to None) def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, From 631ba464f3512fd628aa364b3159f6ae2fffadee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Oct 2018 14:40:03 +0200 Subject: [PATCH 04/26] Show warning if part of backtest data is missing --- freqtrade/optimize/__init__.py | 8 ++++++ freqtrade/tests/optimize/test_optimize.py | 32 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 74c842427..9178139c9 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -113,6 +113,14 @@ def load_data(datadir: str, for pair in pairs: pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) if pairdata: + if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000: + logger.warning('Missing data at start for pair %s, data starts at %s', + pair, + arrow.get(pairdata[0][0] // 1000).strftime('%Y-%m-%d %H:%M:%S')) + if timerange.stoptype == 'date' and pairdata[-1][0] < timerange.stopts * 1000: + logger.warning('Missing data at end for pair %s, data ends at %s', + pair, + arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S')) result[pair] = pairdata else: logger.warning( diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 77fa3e3b1..1e5e8a94e 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -322,6 +322,38 @@ def test_load_tickerdata_file() -> None: assert _BTC_UNITTEST_LENGTH == len(tickerdata) +def test_load_partial_missing(caplog) -> None: + # Make sure we start fresh - test missing data at start + start = arrow.get('2018-01-01T00:00:00') + end = arrow.get('2018-01-11T00:00:00') + tickerdata = optimize.load_data(None, '5m', ['UNITTEST/BTC'], + refresh_pairs=False, + timerange=TimeRange('date', 'date', + start.timestamp, end.timestamp)) + # timedifference in 5 minutes + td = ((end - start).total_seconds() // 60 // 5) + 1 + assert td != len(tickerdata['UNITTEST/BTC']) + start_real = arrow.get(tickerdata['UNITTEST/BTC'][0][0] / 1000) + assert log_has(f'Missing data at start for pair ' + f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}', + caplog.record_tuples) + # Make sure we start fresh - test missing data at end + caplog.clear() + start = arrow.get('2018-01-10T00:00:00') + end = arrow.get('2018-02-20T00:00:00') + tickerdata = optimize.load_data(None, '5m', ['UNITTEST/BTC'], + refresh_pairs=False, + timerange=TimeRange('date', 'date', + start.timestamp, end.timestamp)) + # timedifference in 5 minutes + td = ((end - start).total_seconds() // 60 // 5) + 1 + assert td != len(tickerdata['UNITTEST/BTC']) + end_real = arrow.get(tickerdata['UNITTEST/BTC'][-1][0] / 1000) + assert log_has(f'Missing data at end for pair ' + f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}', + caplog.record_tuples) + + def test_init(default_conf, mocker) -> None: exchange = get_patched_exchange(mocker, default_conf) assert {} == optimize.load_data( From 8a3272e7c54ec31572d039124a8fe052ec83a3d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Oct 2018 19:47:19 +0200 Subject: [PATCH 05/26] don't copy tickerdata_to_dataframe into backtesting it's used only once, so this does not make sense and hides the origin of the function --- freqtrade/optimize/backtesting.py | 3 +-- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/tests/optimize/test_backtesting.py | 16 ++++++++-------- freqtrade/tests/optimize/test_hyperopt.py | 6 +++--- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cd822023f..df2f6834a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -88,7 +88,6 @@ class Backtesting(object): """ self.strategy = strategy self.ticker_interval = self.config.get('ticker_interval') - self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe self.advise_buy = strategy.advise_buy self.advise_sell = strategy.advise_sell @@ -371,7 +370,7 @@ class Backtesting(object): self._set_strategy(strat) # need to reprocess data every time to populate signals - preprocessed = self.tickerdata_to_dataframe(data) + preprocessed = self.strategy.tickerdata_to_dataframe(data) # Print timeframe min_date, max_date = self.get_timeframe(preprocessed) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 4a239ab28..b2d05d603 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -352,7 +352,7 @@ class Hyperopt(Backtesting): if self.has_space('buy'): self.strategy.advise_indicators = Hyperopt.populate_indicators # type: ignore - dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) + dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) self.exchange = None # type: ignore self.load_previous_results() diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index a17867b3a..36b2fcdd3 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -89,7 +89,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None: backtesting = Backtesting(config) data = load_data_test(contour) - processed = backtesting.tickerdata_to_dataframe(data) + processed = backtesting.strategy.tickerdata_to_dataframe(data) assert isinstance(processed, dict) results = backtesting.backtest( { @@ -125,7 +125,7 @@ def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None): backtesting = Backtesting(conf) return { 'stake_amount': conf['stake_amount'], - 'processed': backtesting.tickerdata_to_dataframe(data), + 'processed': backtesting.strategy.tickerdata_to_dataframe(data), 'max_open_trades': 10, 'position_stacking': False, 'record': record @@ -313,7 +313,7 @@ def test_backtesting_init(mocker, default_conf) -> None: backtesting = Backtesting(default_conf) assert backtesting.config == default_conf assert backtesting.ticker_interval == '5m' - assert callable(backtesting.tickerdata_to_dataframe) + assert callable(backtesting.strategy.tickerdata_to_dataframe) assert callable(backtesting.advise_buy) assert callable(backtesting.advise_sell) get_fee.assert_called() @@ -327,7 +327,7 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None: tickerlist = {'UNITTEST/BTC': tick} backtesting = Backtesting(default_conf) - data = backtesting.tickerdata_to_dataframe(tickerlist) + data = backtesting.strategy.tickerdata_to_dataframe(tickerlist) assert len(data['UNITTEST/BTC']) == 99 # Load strategy to compare the result between Backtesting function and strategy are the same @@ -340,7 +340,7 @@ def test_get_timeframe(default_conf, mocker) -> None: patch_exchange(mocker) backtesting = Backtesting(default_conf) - data = backtesting.tickerdata_to_dataframe( + data = backtesting.strategy.tickerdata_to_dataframe( optimize.load_data( None, ticker_interval='1m', @@ -520,7 +520,7 @@ def test_backtest(default_conf, fee, mocker) -> None: pair = 'UNITTEST/BTC' data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC']) data = trim_dictlist(data, -200) - data_processed = backtesting.tickerdata_to_dataframe(data) + data_processed = backtesting.strategy.tickerdata_to_dataframe(data) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], @@ -571,7 +571,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], - 'processed': backtesting.tickerdata_to_dataframe(data), + 'processed': backtesting.strategy.tickerdata_to_dataframe(data), 'max_open_trades': 1, 'position_stacking': False } @@ -585,7 +585,7 @@ def test_processed(default_conf, mocker) -> None: backtesting = Backtesting(default_conf) dict_of_tickerrows = load_data_test('raise') - dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows) + dataframes = backtesting.strategy.tickerdata_to_dataframe(dict_of_tickerrows) dataframe = dataframes['UNITTEST/BTC'] cols = dataframe.columns # assert the dataframe got some of the indicator columns diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 2035e23df..c93f2d316 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -194,7 +194,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: default_conf.update({'spaces': 'all'}) hyperopt = Hyperopt(default_conf) - hyperopt.tickerdata_to_dataframe = MagicMock() + hyperopt.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.start() parallel.assert_called_once() @@ -242,7 +242,7 @@ def test_has_space(hyperopt): def test_populate_indicators(hyperopt) -> None: tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tickerlist = {'UNITTEST/BTC': tick} - dataframes = hyperopt.tickerdata_to_dataframe(tickerlist) + dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) # Check if some indicators are generated. We will not test all of them @@ -254,7 +254,7 @@ def test_populate_indicators(hyperopt) -> None: def test_buy_strategy_generator(hyperopt) -> None: tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tickerlist = {'UNITTEST/BTC': tick} - dataframes = hyperopt.tickerdata_to_dataframe(tickerlist) + dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) populate_buy_trend = hyperopt.buy_strategy_generator( From d7459bbbf3c60695c22201b2273c3a77b4385dea Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Oct 2018 19:59:33 +0200 Subject: [PATCH 06/26] refactor get_timeframe out of backtesting class --- freqtrade/optimize/__init__.py | 17 ++++++++++++++ freqtrade/optimize/backtesting.py | 20 ++-------------- freqtrade/tests/optimize/test_backtesting.py | 24 ++++---------------- freqtrade/tests/optimize/test_optimize.py | 19 +++++++++++++++- 4 files changed, 41 insertions(+), 39 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 74c842427..5367f7663 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -11,7 +11,10 @@ except ImportError: import logging import os from typing import Optional, List, Dict, Tuple, Any +import operator + import arrow +from pandas import DataFrame from freqtrade import misc, constants, OperationalException from freqtrade.exchange import Exchange @@ -59,6 +62,20 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: return tickerlist[start_index:stop_index] +def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: + """ + Get the maximum timeframe for the given backtest data + :param data: dictionary with preprocessed backtesting data + :return: tuple containing min_date, max_date + """ + timeframe = [ + (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) + for frame in data.values() + ] + return min(timeframe, key=operator.itemgetter(0))[0], \ + max(timeframe, key=operator.itemgetter(1))[1] + + def load_tickerdata_file( datadir: str, pair: str, ticker_interval: str, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index df2f6834a..695a52052 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -4,14 +4,12 @@ This module contains the backtesting logic """ import logging -import operator from argparse import Namespace from copy import deepcopy from datetime import datetime, timedelta from pathlib import Path -from typing import Any, Dict, List, NamedTuple, Optional, Tuple +from typing import Any, Dict, List, NamedTuple, Optional -import arrow from pandas import DataFrame from tabulate import tabulate @@ -91,20 +89,6 @@ class Backtesting(object): self.advise_buy = strategy.advise_buy self.advise_sell = strategy.advise_sell - @staticmethod - def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: - """ - Get the maximum timeframe for the given backtest data - :param data: dictionary with preprocessed backtesting data - :return: tuple containing min_date, max_date - """ - timeframe = [ - (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) - for frame in data.values() - ] - return min(timeframe, key=operator.itemgetter(0))[0], \ - max(timeframe, key=operator.itemgetter(1))[1] - def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame, skip_nan: bool = False) -> str: """ @@ -373,7 +357,7 @@ class Backtesting(object): preprocessed = self.strategy.tickerdata_to_dataframe(data) # Print timeframe - min_date, max_date = self.get_timeframe(preprocessed) + min_date, max_date = optimize.get_timeframe(preprocessed) logger.info( 'Measuring data from %s up to %s (%s days)..', min_date.isoformat(), diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 36b2fcdd3..ff6a0666d 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -336,22 +336,6 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None: assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC']) -def test_get_timeframe(default_conf, mocker) -> None: - patch_exchange(mocker) - backtesting = Backtesting(default_conf) - - data = backtesting.strategy.tickerdata_to_dataframe( - optimize.load_data( - None, - ticker_interval='1m', - pairs=['UNITTEST/BTC'] - ) - ) - min_date, max_date = backtesting.get_timeframe(data) - assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' - assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' - - def test_generate_text_table(default_conf, mocker): patch_exchange(mocker) backtesting = Backtesting(default_conf) @@ -451,17 +435,17 @@ def test_generate_text_table_strategyn(default_conf, mocker): def test_backtesting_start(default_conf, mocker, caplog) -> None: - def get_timeframe(input1, input2): + def get_timeframe(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.optimize.load_data', mocked_load_data) + mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.optimize.backtesting.Backtesting', backtest=MagicMock(), _generate_text_table=MagicMock(return_value='1'), - get_timeframe=get_timeframe, ) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] @@ -486,17 +470,17 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: - def get_timeframe(input1, input2): + def get_timeframe(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={})) + mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe) mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.optimize.backtesting.Backtesting', backtest=MagicMock(), _generate_text_table=MagicMock(return_value='1'), - get_timeframe=get_timeframe, ) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 77fa3e3b1..061caf70b 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -15,7 +15,8 @@ from freqtrade.optimize.__init__ import (download_backtesting_testdata, load_cached_data_for_updating, load_tickerdata_file, make_testdata_path, trim_tickerlist) -from freqtrade.tests.conftest import get_patched_exchange, log_has +from freqtrade.strategy.default_strategy import DefaultStrategy +from freqtrade.tests.conftest import get_patched_exchange, log_has, patch_exchange # Change this if modifying UNITTEST/BTC testdatafile _BTC_UNITTEST_LENGTH = 13681 @@ -433,3 +434,19 @@ def test_file_dump_json() -> None: # Remove the file _clean_test_file(file) + + +def test_get_timeframe(default_conf, mocker) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + data = strategy.tickerdata_to_dataframe( + optimize.load_data( + None, + ticker_interval='1m', + pairs=['UNITTEST/BTC'] + ) + ) + min_date, max_date = optimize.get_timeframe(data) + assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' + assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' From fb52d322966382e09c39367c2a10ffbd8efc0aaa Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Oct 2018 19:42:54 +0200 Subject: [PATCH 07/26] Add validate_backtest_data function --- freqtrade/optimize/__init__.py | 19 ++++++++++ freqtrade/optimize/backtesting.py | 4 ++- freqtrade/tests/optimize/test_optimize.py | 44 ++++++++++++++++++++++- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 5367f7663..d4cb6c067 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -10,6 +10,7 @@ except ImportError: _UJSON = False import logging import os +from datetime import datetime from typing import Optional, List, Dict, Tuple, Any import operator @@ -76,6 +77,24 @@ def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow] max(timeframe, key=operator.itemgetter(1))[1] +def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, + max_date: datetime, ticker_interval_mins: int) -> None: + """ + Validates preprocessed backtesting data for missing values and shows warnings about it that. + + :param data: dictionary with preprocessed backtesting data + :param min_date: start-date of the data + :param max_date: end-date of the data + :param ticker_interval_mins: ticker interval in minutes + """ + # total difference in minutes / interval-minutes + expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) + for pair, df in data.items(): + if len(df) < expected_frames: + logger.warning('%s has missing frames: expected %s, got %s', + pair, expected_frames, len(df)) + + def load_tickerdata_file( datadir: str, pair: str, ticker_interval: str, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 695a52052..961cfb092 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -356,8 +356,10 @@ class Backtesting(object): # need to reprocess data every time to populate signals preprocessed = self.strategy.tickerdata_to_dataframe(data) - # Print timeframe min_date, max_date = optimize.get_timeframe(preprocessed) + # Validate dataframe for missing values + optimize.validate_backtest_data(preprocessed, min_date, max_date, + constants.TICKER_INTERVAL_MINUTES[self.ticker_interval]) logger.info( 'Measuring data from %s up to %s (%s days)..', min_date.isoformat(), diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 061caf70b..7b13b498a 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -7,7 +7,7 @@ from shutil import copyfile import arrow -from freqtrade import optimize +from freqtrade import optimize, constants from freqtrade.arguments import TimeRange from freqtrade.misc import file_dump_json from freqtrade.optimize.__init__ import (download_backtesting_testdata, @@ -450,3 +450,45 @@ def test_get_timeframe(default_conf, mocker) -> None: min_date, max_date = optimize.get_timeframe(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' + + +def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + data = strategy.tickerdata_to_dataframe( + optimize.load_data( + None, + ticker_interval='1m', + pairs=['UNITTEST/BTC'] + ) + ) + min_date, max_date = optimize.get_timeframe(data) + caplog.clear() + optimize.validate_backtest_data(data, min_date, max_date, + constants.TICKER_INTERVAL_MINUTES["1m"]) + assert len(caplog.record_tuples) == 1 + assert log_has('UNITTEST/BTC has missing frames: expected 14396, got 13680', + caplog.record_tuples) + + +def test_validate_backtest_data(default_conf, mocker, caplog) -> None: + patch_exchange(mocker) + strategy = DefaultStrategy(default_conf) + + timerange = TimeRange('index', 'index', 200, 250) + data = strategy.tickerdata_to_dataframe( + optimize.load_data( + None, + ticker_interval='5m', + pairs=['UNITTEST/BTC'], + timerange=timerange + ) + ) + + min_date, max_date = optimize.get_timeframe(data) + caplog.clear() + optimize.validate_backtest_data(data, min_date, max_date, + constants.TICKER_INTERVAL_MINUTES["5m"]) + assert len(caplog.record_tuples) == 0 + From 518dcf5209034e4c7fdf14f99257d406363fdcdb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Oct 2018 19:43:04 +0200 Subject: [PATCH 08/26] Cleanup some tests 8m is not a valid ticker value not in constants.TICKER_INTERVAL_MINUTES map --- freqtrade/tests/optimize/test_backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index ff6a0666d..83fe52ed3 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -119,7 +119,7 @@ def _load_pair_as_ticks(pair, tickfreq): # FIX: fixturize this? def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None): - data = optimize.load_data(None, ticker_interval='8m', pairs=[pair]) + data = optimize.load_data(None, ticker_interval='1m', pairs=[pair]) data = trim_dictlist(data, -201) patch_exchange(mocker) backtesting = Backtesting(conf) @@ -449,7 +449,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: ) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] - default_conf['ticker_interval'] = 1 + default_conf['ticker_interval'] = "1m" default_conf['live'] = False default_conf['datadir'] = None default_conf['export'] = None From bc356c4d6511034f691793f54f26c59fdf2a2dc9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Oct 2018 19:48:54 +0200 Subject: [PATCH 09/26] Return true/false for validation function --- freqtrade/optimize/__init__.py | 5 ++++- freqtrade/tests/optimize/test_optimize.py | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index d4cb6c067..3376d0075 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -78,7 +78,7 @@ def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow] def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, - max_date: datetime, ticker_interval_mins: int) -> None: + max_date: datetime, ticker_interval_mins: int) -> bool: """ Validates preprocessed backtesting data for missing values and shows warnings about it that. @@ -89,10 +89,13 @@ def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, """ # total difference in minutes / interval-minutes expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) + found_missing = False for pair, df in data.items(): if len(df) < expected_frames: + found_missing = True logger.warning('%s has missing frames: expected %s, got %s', pair, expected_frames, len(df)) + return found_missing def load_tickerdata_file( diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 7b13b498a..32db5abd0 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -465,8 +465,8 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: ) min_date, max_date = optimize.get_timeframe(data) caplog.clear() - optimize.validate_backtest_data(data, min_date, max_date, - constants.TICKER_INTERVAL_MINUTES["1m"]) + assert optimize.validate_backtest_data(data, min_date, max_date, + constants.TICKER_INTERVAL_MINUTES["1m"]) assert len(caplog.record_tuples) == 1 assert log_has('UNITTEST/BTC has missing frames: expected 14396, got 13680', caplog.record_tuples) @@ -488,7 +488,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog) -> None: min_date, max_date = optimize.get_timeframe(data) caplog.clear() - optimize.validate_backtest_data(data, min_date, max_date, - constants.TICKER_INTERVAL_MINUTES["5m"]) + assert not optimize.validate_backtest_data(data, min_date, max_date, + constants.TICKER_INTERVAL_MINUTES["5m"]) assert len(caplog.record_tuples) == 0 From 3c6d10f03e95b4ede9d63328e4d2f0a65cd2a294 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Oct 2018 20:05:57 +0200 Subject: [PATCH 10/26] Print missing value count too --- freqtrade/optimize/__init__.py | 7 ++++--- freqtrade/tests/optimize/test_optimize.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 3376d0075..e837a09bd 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -91,10 +91,11 @@ def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime, expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) found_missing = False for pair, df in data.items(): - if len(df) < expected_frames: + dflen = len(df) + if dflen < expected_frames: found_missing = True - logger.warning('%s has missing frames: expected %s, got %s', - pair, expected_frames, len(df)) + logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values", + pair, expected_frames, dflen, expected_frames - dflen) return found_missing diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 32db5abd0..2975a3e4b 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -468,8 +468,9 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: assert optimize.validate_backtest_data(data, min_date, max_date, constants.TICKER_INTERVAL_MINUTES["1m"]) assert len(caplog.record_tuples) == 1 - assert log_has('UNITTEST/BTC has missing frames: expected 14396, got 13680', - caplog.record_tuples) + assert log_has( + "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", + caplog.record_tuples) def test_validate_backtest_data(default_conf, mocker, caplog) -> None: @@ -491,4 +492,3 @@ def test_validate_backtest_data(default_conf, mocker, caplog) -> None: assert not optimize.validate_backtest_data(data, min_date, max_date, constants.TICKER_INTERVAL_MINUTES["5m"]) assert len(caplog.record_tuples) == 0 - From 7f9f53248c1aa87a2d7483a9e465d0fc93d68b1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Oct 2018 20:25:21 +0200 Subject: [PATCH 11/26] Add validate_backtest_data script --- scripts/plot_dataframe.py | 381 -------------------------------------- 1 file changed, 381 deletions(-) delete mode 100755 scripts/plot_dataframe.py diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py deleted file mode 100755 index 68713f296..000000000 --- a/scripts/plot_dataframe.py +++ /dev/null @@ -1,381 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to display when the bot will buy a specific pair - -Mandatory Cli parameters: --p / --pair: pair to examine - -Option but recommended --s / --strategy: strategy to use - - -Optional Cli parameters --d / --datadir: path to pair backtest data ---timerange: specify what timerange of data to use. --l / --live: Live, to download the latest ticker for the pair --db / --db-url: Show trades stored in database - - -Indicators recommended -Row 1: sma, ema3, ema5, ema10, ema50 -Row 3: macd, rsi, fisher_rsi, mfi, slowd, slowk, fastd, fastk - -Example of usage: -> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3 ---indicators2 fastk,fastd -""" -import json -import logging -import sys -from argparse import Namespace -from pathlib import Path -from typing import Dict, List, Any - -import pandas as pd -import plotly.graph_objs as go -import pytz - -from plotly import tools -from plotly.offline import plot - -import freqtrade.optimize as optimize -from freqtrade import persistence -from freqtrade.arguments import Arguments, TimeRange -from freqtrade.exchange import Exchange -from freqtrade.optimize.backtesting import setup_configuration -from freqtrade.persistence import Trade -from freqtrade.strategy.resolver import StrategyResolver - -logger = logging.getLogger(__name__) -_CONF: Dict[str, Any] = {} - -timeZone = pytz.UTC - - -def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: - trades: pd.DataFrame = pd.DataFrame() - if args.db_url: - persistence.init(_CONF) - columns = ["pair", "profit", "opents", "closets", "open_rate", "close_rate", "duration"] - - for x in Trade.query.all(): - print("date: {}".format(x.open_date)) - - trades = pd.DataFrame([(t.pair, t.calc_profit(), - t.open_date.replace(tzinfo=timeZone), - t.close_date.replace(tzinfo=timeZone) if t.close_date else None, - t.open_rate, t.close_rate, - t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None) - for t in Trade.query.filter(Trade.pair.is_(pair)).all()], - columns=columns) - - elif args.exportfilename: - file = Path(args.exportfilename) - # must align with columns in backtest.py - columns = ["pair", "profit", "opents", "closets", "index", "duration", - "open_rate", "close_rate", "open_at_end", "sell_reason"] - with file.open() as f: - data = json.load(f) - trades = pd.DataFrame(data, columns=columns) - trades = trades.loc[trades["pair"] == pair] - if timerange: - if timerange.starttype == 'date': - trades = trades.loc[trades["opents"] >= timerange.startts] - if timerange.stoptype == 'date': - trades = trades.loc[trades["opents"] <= timerange.stopts] - - trades['opents'] = pd.to_datetime(trades['opents'], - unit='s', - utc=True, - infer_datetime_format=True) - trades['closets'] = pd.to_datetime(trades['closets'], - unit='s', - utc=True, - infer_datetime_format=True) - return trades - - -def plot_analyzed_dataframe(args: Namespace) -> None: - """ - Calls analyze() and plots the returned dataframe - :return: None - """ - global _CONF - - # Load the configuration - _CONF.update(setup_configuration(args)) - - print(_CONF) - # Set the pair to audit - pair = args.pair - - if pair is None: - logger.critical('Parameter --pair mandatory;. E.g --pair ETH/BTC') - exit() - - if '/' not in pair: - logger.critical('--pair format must be XXX/YYY') - exit() - - # Set timerange to use - timerange = Arguments.parse_timerange(args.timerange) - - # Load the strategy - try: - strategy = StrategyResolver(_CONF).strategy - exchange = Exchange(_CONF) - except AttributeError: - logger.critical( - 'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"', - args.strategy - ) - exit() - - # Set the ticker to use - tick_interval = strategy.ticker_interval - - # Load pair tickers - tickers = {} - if args.live: - logger.info('Downloading pair.') - exchange.refresh_tickers([pair], tick_interval) - tickers[pair] = exchange.klines[pair] - else: - tickers = optimize.load_data( - datadir=_CONF.get("datadir"), - pairs=[pair], - ticker_interval=tick_interval, - refresh_pairs=_CONF.get('refresh_pairs', False), - timerange=timerange, - exchange=Exchange(_CONF) - ) - - # No ticker found, or impossible to download - if tickers == {}: - exit() - - # Get trades already made from the DB - trades = load_trades(args, pair, timerange) - - dataframes = strategy.tickerdata_to_dataframe(tickers) - - dataframe = dataframes[pair] - dataframe = strategy.advise_buy(dataframe, {'pair': pair}) - dataframe = strategy.advise_sell(dataframe, {'pair': pair}) - - if len(dataframe.index) > args.plot_limit: - logger.warning('Ticker contained more than %s candles as defined ' - 'with --plot-limit, clipping.', args.plot_limit) - dataframe = dataframe.tail(args.plot_limit) - - trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']] - fig = generate_graph( - pair=pair, - trades=trades, - data=dataframe, - args=args - ) - - plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html'))) - - -def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tools.make_subplots: - """ - Generate the graph from the data generated by Backtesting or from DB - :param pair: Pair to Display on the graph - :param trades: All trades created - :param data: Dataframe - :param args: sys.argv that contrains the two params indicators1, and indicators2 - :return: None - """ - - # Define the graph - fig = tools.make_subplots( - rows=3, - cols=1, - shared_xaxes=True, - row_width=[1, 1, 4], - vertical_spacing=0.0001, - ) - fig['layout'].update(title=pair) - fig['layout']['yaxis1'].update(title='Price') - fig['layout']['yaxis2'].update(title='Volume') - fig['layout']['yaxis3'].update(title='Other') - - # Common information - candles = go.Candlestick( - x=data.date, - open=data.open, - high=data.high, - low=data.low, - close=data.close, - name='Price' - ) - - df_buy = data[data['buy'] == 1] - buys = go.Scattergl( - x=df_buy.date, - y=df_buy.close, - mode='markers', - name='buy', - marker=dict( - symbol='triangle-up-dot', - size=9, - line=dict(width=1), - color='green', - ) - ) - df_sell = data[data['sell'] == 1] - sells = go.Scattergl( - x=df_sell.date, - y=df_sell.close, - mode='markers', - name='sell', - marker=dict( - symbol='triangle-down-dot', - size=9, - line=dict(width=1), - color='red', - ) - ) - - trade_buys = go.Scattergl( - x=trades["opents"], - y=trades["open_rate"], - mode='markers', - name='trade_buy', - marker=dict( - symbol='square-open', - size=11, - line=dict(width=2), - color='green' - ) - ) - trade_sells = go.Scattergl( - x=trades["closets"], - y=trades["close_rate"], - mode='markers', - name='trade_sell', - marker=dict( - symbol='square-open', - size=11, - line=dict(width=2), - color='red' - ) - ) - - # Row 1 - fig.append_trace(candles, 1, 1) - - if 'bb_lowerband' in data and 'bb_upperband' in data: - bb_lower = go.Scatter( - x=data.date, - y=data.bb_lowerband, - name='BB lower', - line={'color': 'rgba(255,255,255,0)'}, - ) - bb_upper = go.Scatter( - x=data.date, - y=data.bb_upperband, - name='BB upper', - fill="tonexty", - fillcolor="rgba(0,176,246,0.2)", - line={'color': 'rgba(255,255,255,0)'}, - ) - fig.append_trace(bb_lower, 1, 1) - fig.append_trace(bb_upper, 1, 1) - - fig = generate_row(fig=fig, row=1, raw_indicators=args.indicators1, data=data) - fig.append_trace(buys, 1, 1) - fig.append_trace(sells, 1, 1) - fig.append_trace(trade_buys, 1, 1) - fig.append_trace(trade_sells, 1, 1) - - # Row 2 - volume = go.Bar( - x=data['date'], - y=data['volume'], - name='Volume' - ) - fig.append_trace(volume, 2, 1) - - # Row 3 - fig = generate_row(fig=fig, row=3, raw_indicators=args.indicators2, data=data) - - return fig - - -def generate_row(fig, row, raw_indicators, data) -> tools.make_subplots: - """ - Generator all the indicator selected by the user for a specific row - """ - for indicator in raw_indicators.split(','): - if indicator in data: - scattergl = go.Scattergl( - x=data['date'], - y=data[indicator], - name=indicator - ) - fig.append_trace(scattergl, row, 1) - else: - logger.info( - 'Indicator "%s" ignored. Reason: This indicator is not found ' - 'in your strategy.', - indicator - ) - - return fig - - -def plot_parse_args(args: List[str]) -> Namespace: - """ - Parse args passed to the script - :param args: Cli arguments - :return: args: Array with all arguments - """ - arguments = Arguments(args, 'Graph dataframe') - arguments.scripts_options() - arguments.parser.add_argument( - '--indicators1', - help='Set indicators from your strategy you want in the first row of the graph. Separate ' - 'them with a coma. E.g: ema3,ema5 (default: %(default)s)', - type=str, - default='sma,ema3,ema5', - dest='indicators1', - ) - - arguments.parser.add_argument( - '--indicators2', - help='Set indicators from your strategy you want in the third row of the graph. Separate ' - 'them with a coma. E.g: fastd,fastk (default: %(default)s)', - type=str, - default='macd', - dest='indicators2', - ) - arguments.parser.add_argument( - '--plot-limit', - help='Specify tick limit for plotting - too high values cause huge files - ' - 'Default: %(default)s', - dest='plot_limit', - default=750, - type=int, - ) - arguments.common_args_parser() - arguments.optimizer_shared_options(arguments.parser) - arguments.backtesting_options(arguments.parser) - return arguments.parse_args() - - -def main(sysargv: List[str]) -> None: - """ - This function will initiate the bot and start the trading loop. - :return: None - """ - logger.info('Starting Plot Dataframe') - plot_analyzed_dataframe( - plot_parse_args(sysargv) - ) - - -if __name__ == '__main__': - main(sys.argv[1:]) From f860aab094019e3a5fabe7c0861bda945e76b194 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 26 Oct 2018 14:33:07 +0200 Subject: [PATCH 12/26] Update ccxt from 1.17.411 to 1.17.421 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a08f55beb..40ab21f61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.411 +ccxt==1.17.421 SQLAlchemy==1.2.12 python-telegram-bot==11.1.0 arrow==0.12.1 From 57d3a6f7a7c39faeee577481025e5a6bca73d7e5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 27 Oct 2018 14:33:06 +0200 Subject: [PATCH 13/26] Update ccxt from 1.17.421 to 1.17.427 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 40ab21f61..79662c483 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.421 +ccxt==1.17.427 SQLAlchemy==1.2.12 python-telegram-bot==11.1.0 arrow==0.12.1 From a4fc5afb66bc6adf3e638469b2c1196ca1194ac2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Oct 2018 17:35:08 +0200 Subject: [PATCH 14/26] Add hyperopt ROI documentation, add note on methology for hyperopt --- docs/hyperopt.md | 62 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index d1f363733..51fcbfaad 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -1,4 +1,5 @@ # Hyperopt + This page explains how to tune your strategy by finding the optimal parameters, a process called hyperparameter optimization. The bot uses several algorithms included in the `scikit-optimize` package to accomplish this. The @@ -8,17 +9,20 @@ and still take a long time. *Note:* Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) ## Table of Contents + - [Prepare your Hyperopt](#prepare-hyperopt) - [Configure your Guards and Triggers](#configure-your-guards-and-triggers) - [Solving a Mystery](#solving-a-mystery) - [Adding New Indicators](#adding-new-indicators) - [Execute Hyperopt](#execute-hyperopt) -- [Understand the hyperopts result](#understand-the-backtesting-result) +- [Understand the hyperopt result](#understand-the-hyperopt-result) ## Prepare Hyperopting + We recommend you start by taking a look at `hyperopt.py` file located in [freqtrade/optimize](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py) ### Configure your Guards and Triggers + There are two places you need to change to add a new buy strategy for testing: - Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L231-L264). - Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L213-L224) @@ -113,11 +117,12 @@ When you want to test an indicator that isn't used by the bot currently, remembe add it to the `populate_indicators()` method in `hyperopt.py`. ## Execute Hyperopt -Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combination to find the best parameters -it will take time you will have the result (more than 30 mins). -We strongly recommend to use `screen` to prevent any connection loss. +Once you have updated your hyperopt configuration you can run it. +Because hyperopt tries a lot of combinations to find the best parameters it will take time you will have the result (more than 30 mins). + +We strongly recommend to use `screen` or `tmux` to prevent any connection loss. + ```bash python3 ./freqtrade/main.py -c config.json hyperopt -e 5000 ``` @@ -126,11 +131,13 @@ The `-e` flag will set how many evaluations hyperopt will do. We recommend running at least several thousand evaluations. ### Execute Hyperopt with Different Ticker-Data Source + If you would like to hyperopt parameters using an alternate ticker data that you have on-disk, use the `--datadir PATH` option. Default hyperopt will use data from directory `user_data/data`. ### Running Hyperopt with Smaller Testset + Use the `--timerange` argument to change how much of the testset you want to use. The last N ticks/timeframes will be used. Example: @@ -140,6 +147,7 @@ python3 ./freqtrade/main.py hyperopt --timerange -200 ``` ### Running Hyperopt with Smaller Search Space + Use the `--spaces` argument to limit the search space used by hyperopt. Letting Hyperopt optimize everything is a huuuuge search space. Often it might make more sense to start by just searching for initial buy algorithm. @@ -154,7 +162,8 @@ Legal values are: - `stoploss`: search for the best stoploss value - space-separated list of any of the above values for example `--spaces roi stoploss` -## Understand the Hyperopts Result +## Understand the Hyperopt Result + Once Hyperopt is completed you can use the result to create a new strategy. Given the following result from hyperopt: @@ -166,22 +175,24 @@ with values: ``` You should understand this result like: + - The buy trigger that worked best was `bb_lower`. - You should not use ADX because `adx-enabled: False`) - You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) You have to look inside your strategy file into `buy_strategy_generator()` -method, what those values match to. +method, what those values match to. -So for example you had `rsi-value: 29.0` so we would look -at `rsi`-block, that translates to the following code block: +So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: + ``` (dataframe['rsi'] < 29.0) ``` Translating your whole hyperopt result as the new buy-signal would then look like: -``` + +```python def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: dataframe.loc[ ( @@ -192,6 +203,37 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: return dataframe ``` +### Understand Hyperopt ROI results + +If you are optimizing ROI, you're result will look as follows and include a ROI table. + +``` +Best result: + 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. +with values: +{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower', 'roi_t1': 40, 'roi_t2': 57, 'roi_t3': 21, 'roi_p1': 0.03634636907306948, 'roi_p2': 0.055237357937802885, 'roi_p3': 0.015163796015548354, 'stoploss': -0.37996664668703606} +ROI table: +{0: 0.10674752302642071, 21: 0.09158372701087236, 78: 0.03634636907306948, 118: 0} +``` + +This would translate to the following ROI table: + +``` python + minimal_roi = { + "118": 0, + "78": 0.0363463, + "21": 0.0915, + "0": 0.106 + } +``` + +### Validate backtest result + +Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. +To archive the same results (number of trades, ...) than during hyperopt, please use the command line flag `--disable-max-market-positions`. +This setting is the default for hyperopt for speed reasons. you can overwrite this in the configuration by setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L283) + ## Next Step + Now you have a perfect bot and want to control it from Telegram. Your next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md). From 7e4a0baef252d24d50564061e25433a80a8b0594 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Oct 2018 17:38:15 +0200 Subject: [PATCH 15/26] improve hyperopt.md --- docs/hyperopt.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 51fcbfaad..e2dcf3e95 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -231,7 +231,9 @@ This would translate to the following ROI table: Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. To archive the same results (number of trades, ...) than during hyperopt, please use the command line flag `--disable-max-market-positions`. -This setting is the default for hyperopt for speed reasons. you can overwrite this in the configuration by setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L283) +This setting is the default for hyperopt for speed reasons. You can overwrite this in the configuration by setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L283). + +Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality. ## Next Step From 551dc79cf7692191b797351fb03ebf67a5771c2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Oct 2018 13:15:49 +0100 Subject: [PATCH 16/26] Don't overwrite pair_whitelist in config dict Doing that will result in an empty whitelist after a short Exchange downtime --- freqtrade/freqtradebot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fa803bda7..3881f78d1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -54,6 +54,7 @@ class FreqtradeBot(object): self.rpc: RPCManager = RPCManager(self) self.persistence = None self.exchange = Exchange(self.config) + self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist'] self._init_modules() def _init_modules(self) -> None: @@ -179,11 +180,10 @@ class FreqtradeBot(object): ) # Keep only the subsets of pairs wanted (up to nb_assets) - final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list - self.config['exchange']['pair_whitelist'] = final_list + self.active_pair_whitelist = sanitized_list[:nb_assets] if nb_assets else sanitized_list # Refreshing candles - self.exchange.refresh_tickers(final_list, self.strategy.ticker_interval) + self.exchange.refresh_tickers(self.active_pair_whitelist, self.strategy.ticker_interval) # Query trades from persistence layer trades = Trade.query.filter(Trade.is_open.is_(True)).all() @@ -380,7 +380,7 @@ class FreqtradeBot(object): 'Checking buy signals to create a new trade with stake_amount: %f ...', stake_amount ) - whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist']) + whitelist = copy.deepcopy(self.active_pair_whitelist) # Remove currently opened and latest pairs from whitelist for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): From d3387dec456b3dfafacfcc29c11a1b34ed5d4ffd Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 28 Oct 2018 13:33:09 +0100 Subject: [PATCH 17/26] Update ccxt from 1.17.427 to 1.17.429 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 79662c483..e69ca09c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.427 +ccxt==1.17.429 SQLAlchemy==1.2.12 python-telegram-bot==11.1.0 arrow==0.12.1 From 86ad0c047c79bc7f38b86d08fb34b003faa5ac90 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 28 Oct 2018 13:33:10 +0100 Subject: [PATCH 18/26] Update pytest from 3.9.2 to 3.9.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e69ca09c2..d9eb58c25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ scipy==1.1.0 jsonschema==2.6.0 numpy==1.15.3 TA-Lib==0.4.17 -pytest==3.9.2 +pytest==3.9.3 pytest-mock==1.10.0 pytest-asyncio==0.9.0 pytest-cov==2.6.0 From 1121ec0724f0d69ee1d7c8b5791882cd8e6db01e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Oct 2018 14:43:35 +0100 Subject: [PATCH 19/26] don't have nb_assets as parameter - it's a config setting as any other --- freqtrade/freqtradebot.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3881f78d1..ca503c89c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -108,11 +108,8 @@ class FreqtradeBot(object): constants.PROCESS_THROTTLE_SECS ) - nb_assets = self.config.get('dynamic_whitelist', None) - self._throttle(func=self._process, - min_secs=min_secs, - nb_assets=nb_assets) + min_secs=min_secs) return state def _startup_messages(self) -> None: @@ -163,15 +160,15 @@ class FreqtradeBot(object): time.sleep(duration) return result - def _process(self, nb_assets: Optional[int] = 0) -> bool: + def _process(self) -> bool: """ Queries the persistence layer for open trades and handles them, otherwise a new trade is created. - :param: nb_assets: the maximum number of pairs to be traded at the same time :return: True if one or more trades has been created or closed, False otherwise """ state_changed = False try: + nb_assets = self.config.get('dynamic_whitelist', None) # Refresh whitelist based on wallet maintenance sanitized_list = self._refresh_whitelist( self._gen_pair_whitelist( From 35759b372d336ba6d6add798b439c72eefe01590 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Oct 2018 13:33:10 +0100 Subject: [PATCH 20/26] Update ccxt from 1.17.429 to 1.17.432 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d9eb58c25..da382e82b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.429 +ccxt==1.17.432 SQLAlchemy==1.2.12 python-telegram-bot==11.1.0 arrow==0.12.1 From 2f55cbde35ebc839df94e414e7ddd02554a9c5be Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Oct 2018 19:23:56 +0100 Subject: [PATCH 21/26] fix #1298 --- freqtrade/freqtradebot.py | 11 +++++-- freqtrade/tests/test_freqtradebot.py | 46 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ca503c89c..6cc46a07e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -179,12 +179,17 @@ class FreqtradeBot(object): # Keep only the subsets of pairs wanted (up to nb_assets) self.active_pair_whitelist = sanitized_list[:nb_assets] if nb_assets else sanitized_list - # Refreshing candles - self.exchange.refresh_tickers(self.active_pair_whitelist, self.strategy.ticker_interval) - # Query trades from persistence layer trades = Trade.query.filter(Trade.is_open.is_(True)).all() + # Extend active-pair whitelist with pairs from open trades + # ensures that tickers are downloaded for open trades + self.active_pair_whitelist.extend([trade.pair for trade in trades + if trade.pair not in self.active_pair_whitelist]) + + # Refreshing candles + self.exchange.refresh_tickers(self.active_pair_whitelist, self.strategy.ticker_interval) + # First process current opened trades for trade in trades: state_changed |= self.process_maybe_execute_sell(trade) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 6b13da35f..871e59240 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -663,6 +663,52 @@ def test_process_trade_handling( assert result is False +def test_process_trade_no_whitelist_pair( + default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None: + """ Test _process with trade not in pair list """ + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + get_markets=markets, + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_order=MagicMock(return_value=limit_buy_order), + get_fee=fee, + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + pair = 'NOCLUE/BTC' + # create open trade not in whitelist + Trade.session.add(Trade( + pair=pair, + stake_amount=0.001, + fee_open=fee.return_value, + fee_close=fee.return_value, + is_open=True, + amount=20, + open_rate=0.01, + exchange='bittrex', + )) + Trade.session.add(Trade( + pair='ETH/BTC', + stake_amount=0.001, + fee_open=fee.return_value, + fee_close=fee.return_value, + is_open=True, + amount=12, + open_rate=0.001, + exchange='bittrex', + )) + + assert pair not in freqtrade.active_pair_whitelist + result = freqtrade._process() + assert pair in freqtrade.active_pair_whitelist + # Make sure each pair is only in the list once + assert len(freqtrade.active_pair_whitelist) == len(set(freqtrade.active_pair_whitelist)) + assert result is True + + def test_balance_fully_ask_side(mocker, default_conf) -> None: default_conf['bid_strategy']['ask_last_balance'] = 0.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) From 936441a853bc67a2628e3a8cb36f036de4662968 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 30 Oct 2018 13:33:07 +0100 Subject: [PATCH 22/26] Update ccxt from 1.17.432 to 1.17.436 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index da382e82b..5244b54a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.432 +ccxt==1.17.436 SQLAlchemy==1.2.12 python-telegram-bot==11.1.0 arrow==0.12.1 From eab15e09f5d24444e8d5b1d31cada2b0e881f5c7 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 31 Oct 2018 13:34:07 +0100 Subject: [PATCH 23/26] Update ccxt from 1.17.436 to 1.17.439 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5244b54a8..26a2d7a97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.436 +ccxt==1.17.439 SQLAlchemy==1.2.12 python-telegram-bot==11.1.0 arrow==0.12.1 From 92f9c828e6bc9322adda1036745cdd96e4d08697 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 1 Nov 2018 13:34:07 +0100 Subject: [PATCH 24/26] Update ccxt from 1.17.439 to 1.17.448 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 26a2d7a97..8dffcd5c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.439 +ccxt==1.17.448 SQLAlchemy==1.2.12 python-telegram-bot==11.1.0 arrow==0.12.1 From afc1329126652717e3299ce270185a53985063e0 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 1 Nov 2018 13:34:08 +0100 Subject: [PATCH 25/26] Update sqlalchemy from 1.2.12 to 1.2.13 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8dffcd5c6..c688e3a68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ ccxt==1.17.448 -SQLAlchemy==1.2.12 +SQLAlchemy==1.2.13 python-telegram-bot==11.1.0 arrow==0.12.1 cachetools==2.1.0 From 17895282a1f6ed8ebf03777e752183bf0775820d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 2 Nov 2018 13:34:06 +0100 Subject: [PATCH 26/26] Update ccxt from 1.17.448 to 1.17.455 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c688e3a68..57beabf49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.448 +ccxt==1.17.455 SQLAlchemy==1.2.13 python-telegram-bot==11.1.0 arrow==0.12.1