From e7d043974199ab041ccfd44111c73d330be62f0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Jul 2018 23:00:50 +0200 Subject: [PATCH 01/35] Add new arguments --- freqtrade/arguments.py | 8 ++++++++ freqtrade/configuration.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 022a2c739..042eeedf1 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -142,6 +142,14 @@ class Arguments(object): action='store_true', dest='refresh_pairs', ) + parser.add_argument( + '--strategy-list', + help='Provide a commaseparated list of strategies to backtest ' + 'Please note that ticker-interval needs to be set either in config ' + 'or via command line', + nargs='+', + dest='strategy_list', + ) parser.add_argument( '--export', help='export backtest results, argument are: trades\ diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index dcc6e4332..aa452c79d 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -187,6 +187,14 @@ class Configuration(object): config.update({'refresh_pairs': True}) logger.info('Parameter -r/--refresh-pairs-cached detected ...') + if 'strategy_list' in self.args and self.args.strategy_list: + config.update({'strategy_list': self.args.strategy_list}) + logger.info('using strategy list of %s Strategies', len(self.args.strategy_list)) + + if 'ticker_interval' in self.args and self.args.ticker_interval: + config.update({'ticker_interval': self.args.ticker_interval}) + logger.info('Overriding ticker interval with Command line argument') + # If --export is used we add it to the configuration if 'export' in self.args and self.args.export: config.update({'export': self.args.export}) From 56046b3cb39c16ba8a43e43cca88de2d5ecfa51c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Jul 2018 23:01:52 +0200 Subject: [PATCH 02/35] Add strategylist option to backtesting --- freqtrade/optimize/backtesting.py | 126 +++++++++++++++++------------- 1 file changed, 71 insertions(+), 55 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 593af619c..4146c25dd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -6,6 +6,7 @@ This module contains the backtesting logic import logging import operator from argparse import Namespace +from copy import deepcopy from datetime import datetime, timedelta from typing import Any, Dict, List, NamedTuple, Optional, Tuple @@ -54,11 +55,6 @@ class Backtesting(object): """ def __init__(self, config: Dict[str, Any]) -> None: self.config = config - self.strategy: IStrategy = StrategyResolver(self.config).strategy - self.ticker_interval = self.strategy.ticker_interval - self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe - self.advise_buy = self.strategy.advise_buy - self.advise_sell = self.strategy.advise_sell # Reset keys for backtesting self.config['exchange']['key'] = '' @@ -279,6 +275,19 @@ class Backtesting(object): pairs = self.config['exchange']['pair_whitelist'] logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) + strategylist: List[IStrategy] = [] + if self.config.get('strategy_list', None): + # Force one interval + self.ticker_interval = self.config.get('ticker_interval') + for strat in self.config.get('strategy_list'): + stratconf = deepcopy(self.config) + stratconf['strategy'] = strat + s = StrategyResolver(stratconf).strategy + strategylist.append(s) + + else: + # only one strategy + strategylist.append(StrategyResolver(self.config).strategy) if self.config.get('live'): logger.info('Downloading data for all pairs in whitelist ...') @@ -308,61 +317,68 @@ class Backtesting(object): logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') max_open_trades = 0 - preprocessed = self.tickerdata_to_dataframe(data) + for strat in strategylist: + self.strategy = strat + self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe + self.populate_buy_trend = self.strategy.populate_buy_trend + self.populate_sell_trend = self.strategy.populate_sell_trend - # Print timeframe - min_date, max_date = self.get_timeframe(preprocessed) - logger.info( - 'Measuring data from %s up to %s (%s days)..', - min_date.isoformat(), - max_date.isoformat(), - (max_date - min_date).days - ) + # need to reprocess data every time to populate signals + preprocessed = self.tickerdata_to_dataframe(data) - # Execute backtest and print results - results = self.backtest( - { - 'stake_amount': self.config.get('stake_amount'), - 'processed': preprocessed, - 'max_open_trades': max_open_trades, - 'position_stacking': self.config.get('position_stacking', False), - } - ) - - if self.config.get('export', False): - self._store_backtest_result(self.config.get('exportfilename'), results) - - logger.info( - '\n' + '=' * 49 + - ' BACKTESTING REPORT ' + - '=' * 50 + '\n' - '%s', - self._generate_text_table( - data, - results + # Print timeframe + min_date, max_date = self.get_timeframe(preprocessed) + logger.info( + 'Measuring data from %s up to %s (%s days)..', + min_date.isoformat(), + max_date.isoformat(), + (max_date - min_date).days ) - ) - # logger.info( - # results[['sell_reason']].groupby('sell_reason').count() - # ) - logger.info( - '\n' + - ' SELL READON STATS '.center(119, '=') + - '\n%s \n', - self._generate_text_table_sell_reason(data, results) - - ) - - logger.info( - '\n' + - ' LEFT OPEN TRADES REPORT '.center(119, '=') + - '\n%s', - self._generate_text_table( - data, - results.loc[results.open_at_end] + # Execute backtest and print results + results = self.backtest( + { + 'stake_amount': self.config.get('stake_amount'), + 'processed': preprocessed, + 'max_open_trades': max_open_trades, + 'position_stacking': self.config.get('position_stacking', False), + } + ) + + if self.config.get('export', False): + self._store_backtest_result(self.config.get('exportfilename'), results) + + logger.info( + '\n' + '=' * 49 + + ' BACKTESTING REPORT ' + + '=' * 50 + '\n' + '%s', + self._generate_text_table( + data, + results + ) + ) + # logger.info( + # results[['sell_reason']].groupby('sell_reason').count() + # ) + + logger.info( + '\n' + + ' SELL READON STATS '.center(119, '=') + + '\n%s \n', + self._generate_text_table_sell_reason(data, results) + + ) + + logger.info( + '\n' + + ' LEFT OPEN TRADES REPORT '.center(119, '=') + + '\n%s', + self._generate_text_table( + data, + results.loc[results.open_at_end] + ) ) - ) def setup_configuration(args: Namespace) -> Dict[str, Any]: From 9a42aac0f24e82694e783d146c6069bb24663abe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Jul 2018 06:40:39 +0200 Subject: [PATCH 03/35] Add testcase for --strategylist --- freqtrade/configuration.py | 2 +- freqtrade/tests/test_arguments.py | 8 +++- freqtrade/tests/test_configuration.py | 55 +++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index aa452c79d..3da432b1d 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -189,7 +189,7 @@ class Configuration(object): if 'strategy_list' in self.args and self.args.strategy_list: config.update({'strategy_list': self.args.strategy_list}) - logger.info('using strategy list of %s Strategies', len(self.args.strategy_list)) + logger.info('Using strategy list of %s Strategies', len(self.args.strategy_list)) if 'ticker_interval' in self.args and self.args.ticker_interval: config.update({'ticker_interval': self.args.ticker_interval}) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 79bd0254b..e09aeb1df 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -132,7 +132,11 @@ def test_parse_args_backtesting_custom() -> None: 'backtesting', '--live', '--ticker-interval', '1m', - '--refresh-pairs-cached'] + '--refresh-pairs-cached', + '--strategy-list', + 'DefaultStrategy', + 'TestStrategy' + ] call_args = Arguments(args, '').get_parsed_arg() assert call_args.config == 'test_conf.json' assert call_args.live is True @@ -141,6 +145,8 @@ def test_parse_args_backtesting_custom() -> None: assert call_args.func is not None assert call_args.ticker_interval == '1m' assert call_args.refresh_pairs is True + assert type(call_args.strategy_list) is list + assert len(call_args.strategy_list) == 2 def test_parse_args_hyperopt_custom() -> None: diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index e48553bdf..bf41aab83 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -292,6 +292,61 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non ) +def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None: + """ + Test setup_configuration() function + """ + mocker.patch('freqtrade.configuration.open', mocker.mock_open( + read_data=json.dumps(default_conf) + )) + + arglist = [ + '--config', 'config.json', + 'backtesting', + '--ticker-interval', '1m', + '--export', '/bar/foo', + '--strategy-list', + 'DefaultStrategy', + 'TestStrategy' + ] + + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + config = configuration.get_config() + assert 'max_open_trades' in config + assert 'stake_currency' in config + assert 'stake_amount' in config + assert 'exchange' in config + assert 'pair_whitelist' in config['exchange'] + assert 'datadir' in config + assert log_has( + 'Using data folder: {} ...'.format(config['datadir']), + caplog.record_tuples + ) + assert 'ticker_interval' in config + assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) + assert log_has( + 'Using ticker_interval: 1m ...', + caplog.record_tuples + ) + + assert 'strategy_list' in config + assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples) + + assert 'position_stacking' not in config + + assert 'use_max_market_positions' not in config + + assert 'timerange' not in config + + assert 'export' in config + assert log_has( + 'Parameter --export detected: {} ...'.format(config['export']), + caplog.record_tuples + ) + + def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) From 65aaa3dffdea2ddae22795cdc202cccb1dbca56d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Jul 2018 06:54:33 +0200 Subject: [PATCH 04/35] Extract backtest strategy setting --- freqtrade/optimize/backtesting.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4146c25dd..14a66f2ac 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -65,6 +65,16 @@ class Backtesting(object): self.exchange = Exchange(self.config) self.fee = self.exchange.get_fee() + def set_strategy(self, strategy): + """ + Load strategy into backtesting + """ + self.strategy = strategy + self.ticker_interval = self.config.get('ticker_interval') + self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe + self.populate_buy_trend = strategy.populate_buy_trend + self.populate_sell_trend = strategy.populate_sell_trend + @staticmethod def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: """ @@ -288,6 +298,7 @@ class Backtesting(object): else: # only one strategy strategylist.append(StrategyResolver(self.config).strategy) + self.set_strategy(strategylist[0]) if self.config.get('live'): logger.info('Downloading data for all pairs in whitelist ...') @@ -316,12 +327,11 @@ class Backtesting(object): else: logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') max_open_trades = 0 + all_results = {} for strat in strategylist: - self.strategy = strat - self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe - self.populate_buy_trend = self.strategy.populate_buy_trend - self.populate_sell_trend = self.strategy.populate_sell_trend + logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) + self.set_strategy(strat) # need to reprocess data every time to populate signals preprocessed = self.tickerdata_to_dataframe(data) @@ -336,7 +346,7 @@ class Backtesting(object): ) # Execute backtest and print results - results = self.backtest( + all_results[self.strategy.get_strategy_name()] = self.backtest( { 'stake_amount': self.config.get('stake_amount'), 'processed': preprocessed, @@ -345,14 +355,16 @@ class Backtesting(object): } ) + for strategy, results in all_results.items(): + if self.config.get('export', False): self._store_backtest_result(self.config.get('exportfilename'), results) + logger.info("\nResult for strategy %s", strategy) logger.info( - '\n' + '=' * 49 + - ' BACKTESTING REPORT ' + - '=' * 50 + '\n' - '%s', + '\n' + + ' BACKTESTING REPORT '.center(119, '=') + + '\n%s', self._generate_text_table( data, results From 5f2e92ec5c7329ba3fe5b53ca4b5dd65332c7a2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Jul 2018 07:00:58 +0200 Subject: [PATCH 05/35] Refactor backtesting --- freqtrade/optimize/backtesting.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 14a66f2ac..a9121a3d0 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -53,6 +53,7 @@ class Backtesting(object): backtesting = Backtesting(config) backtesting.start() """ + def __init__(self, config: Dict[str, Any]) -> None: self.config = config @@ -62,10 +63,14 @@ class Backtesting(object): self.config['exchange']['password'] = '' self.config['exchange']['uid'] = '' self.config['dry_run'] = True + if not self.config.get('strategy_list'): + # In Single strategy mode, load strategy here to avoid problems with hyperopt + self._set_strategy(StrategyResolver(self.config).strategy) + self.exchange = Exchange(self.config) self.fee = self.exchange.get_fee() - def set_strategy(self, strategy): + def _set_strategy(self, strategy): """ Load strategy into backtesting """ @@ -297,8 +302,7 @@ class Backtesting(object): else: # only one strategy - strategylist.append(StrategyResolver(self.config).strategy) - self.set_strategy(strategylist[0]) + strategylist.append(self.strategy) if self.config.get('live'): logger.info('Downloading data for all pairs in whitelist ...') @@ -331,7 +335,7 @@ class Backtesting(object): for strat in strategylist: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) - self.set_strategy(strat) + self._set_strategy(strat) # need to reprocess data every time to populate signals preprocessed = self.tickerdata_to_dataframe(data) From 644f729aeabf42cd2ac7da45d43881b6bfdebe96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Jul 2018 07:41:38 +0200 Subject: [PATCH 06/35] Refactor strategy loading to __init__ --- freqtrade/optimize/backtesting.py | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a9121a3d0..ffd89635a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -63,9 +63,22 @@ class Backtesting(object): self.config['exchange']['password'] = '' self.config['exchange']['uid'] = '' self.config['dry_run'] = True - if not self.config.get('strategy_list'): - # In Single strategy mode, load strategy here to avoid problems with hyperopt - self._set_strategy(StrategyResolver(self.config).strategy) + self.strategylist: List[IStrategy] = [] + if self.config.get('strategy_list', None): + # Force one interval + self.ticker_interval = self.config.get('ticker_interval') + for strat in self.config.get('strategy_list'): + stratconf = deepcopy(self.config) + stratconf['strategy'] = strat + self.strategylist.append(StrategyResolver(stratconf).strategy) + + else: + # only one strategy + strat = StrategyResolver(self.config).strategy + + self.strategylist.append(StrategyResolver(self.config).strategy) + # Load one strategy + self._set_strategy(self.strategylist[0]) self.exchange = Exchange(self.config) self.fee = self.exchange.get_fee() @@ -290,19 +303,6 @@ class Backtesting(object): pairs = self.config['exchange']['pair_whitelist'] logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - strategylist: List[IStrategy] = [] - if self.config.get('strategy_list', None): - # Force one interval - self.ticker_interval = self.config.get('ticker_interval') - for strat in self.config.get('strategy_list'): - stratconf = deepcopy(self.config) - stratconf['strategy'] = strat - s = StrategyResolver(stratconf).strategy - strategylist.append(s) - - else: - # only one strategy - strategylist.append(self.strategy) if self.config.get('live'): logger.info('Downloading data for all pairs in whitelist ...') @@ -333,7 +333,7 @@ class Backtesting(object): max_open_trades = 0 all_results = {} - for strat in strategylist: + for strat in self.strategylist: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) From bd3563df6738409d7b0e92e33d879d193a81b16b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Jul 2018 07:55:59 +0200 Subject: [PATCH 07/35] Add test for new functionality --- freqtrade/optimize/backtesting.py | 4 +- freqtrade/tests/optimize/test_backtesting.py | 63 +++++++++++++++++--- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ffd89635a..0bd76b2c4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -66,8 +66,8 @@ class Backtesting(object): self.strategylist: List[IStrategy] = [] if self.config.get('strategy_list', None): # Force one interval - self.ticker_interval = self.config.get('ticker_interval') - for strat in self.config.get('strategy_list'): + self.ticker_interval = str(self.config.get('ticker_interval')) + for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat self.strategylist.append(StrategyResolver(stratconf).strategy) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 5d121d27c..d91781ffc 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -686,15 +686,6 @@ def test_backtest_start_live(default_conf, mocker, caplog): read_data=json.dumps(default_conf) )) - args = MagicMock() - args.ticker_interval = 1 - args.level = 10 - args.live = True - args.datadir = None - args.export = None - args.strategy = 'DefaultStrategy' - args.timerange = '-100' # needed due to MagicMock malleability - args = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', @@ -725,3 +716,57 @@ def test_backtest_start_live(default_conf, mocker, caplog): for line in exists: assert log_has(line, caplog.record_tuples) + + +def test_backtest_start_multi_strat(default_conf, mocker, caplog): + conf = deepcopy(default_conf) + conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', + new=lambda s, n, i: _load_pair_as_ticks(n, i)) + patch_exchange(mocker) + backtestmock = MagicMock() + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) + gen_table_mock = MagicMock() + mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock) + mocker.patch('freqtrade.configuration.open', mocker.mock_open( + read_data=json.dumps(conf) + )) + + args = [ + '--config', 'config.json', + '--datadir', 'freqtrade/tests/testdata', + 'backtesting', + '--ticker-interval', '1m', + '--live', + '--timerange', '-100', + '--enable-position-stacking', + '--disable-max-market-positions', + '--strategy-list', + 'DefaultStrategy', + 'TestStrategy', + ] + args = get_args(args) + start(args) + # 2 backtests, 4 tables + assert backtestmock.call_count == 2 + assert gen_table_mock.call_count == 4 + + # check the logs, that will contain the backtest result + exists = [ + 'Parameter -i/--ticker-interval detected ...', + 'Using ticker_interval: 1m ...', + 'Parameter -l/--live detected ...', + 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', + 'Parameter --timerange detected: -100 ...', + 'Using data folder: freqtrade/tests/testdata ...', + 'Using stake_currency: BTC ...', + 'Using stake_amount: 0.001 ...', + 'Downloading data for all pairs in whitelist ...', + 'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Parameter --enable-position-stacking detected ...', + 'Running backtesting for Strategy DefaultStrategy', + 'Running backtesting for Strategy TestStrategy', + ] + + for line in exists: + assert log_has(line, caplog.record_tuples) From a57a2f4a750b52dcf5871e5aec329a34c5d60f32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Jul 2018 21:55:47 +0200 Subject: [PATCH 08/35] Store backtest-result in different vars --- freqtrade/arguments.py | 4 +++- freqtrade/optimize/backtesting.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 042eeedf1..501c1784f 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -146,7 +146,9 @@ class Arguments(object): '--strategy-list', help='Provide a commaseparated list of strategies to backtest ' 'Please note that ticker-interval needs to be set either in config ' - 'or via command line', + 'or via command line. When using this together with --export trades, ' + 'the strategy-name is injected into the filename ' + '(so backtest-data.json becomes backtest-data-DefaultStrategy.json', nargs='+', dest='strategy_list', ) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0bd76b2c4..69d48b027 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -8,6 +8,7 @@ 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 import arrow @@ -156,7 +157,8 @@ class Backtesting(object): tabular_data.append([reason.value, count]) return tabulate(tabular_data, headers=headers, tablefmt="pipe") - def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None: + def _store_backtest_result(self, recordfilename: str, results: DataFrame, + strategyname: Optional[str] = None) -> None: records = [(t.pair, t.profit_percent, t.open_time.timestamp(), t.close_time.timestamp(), t.open_index - 1, t.trade_duration, @@ -164,6 +166,11 @@ class Backtesting(object): for index, t in results.iterrows()] if records: + if strategyname: + # Inject strategyname to filename + recname = Path(recordfilename) + recordfilename = str(Path.joinpath( + recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix)) logger.info('Dumping backtest results to %s', recordfilename) file_dump_json(recordfilename, records) @@ -362,7 +369,8 @@ class Backtesting(object): for strategy, results in all_results.items(): if self.config.get('export', False): - self._store_backtest_result(self.config.get('exportfilename'), results) + self._store_backtest_result(self.config['exportfilename'], results, + strategy if len(self.strategylist) > 1 else None) logger.info("\nResult for strategy %s", strategy) logger.info( From a8b55b8989387f083250b0b8bc7dbdefd05e4d3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Jul 2018 22:00:12 +0200 Subject: [PATCH 09/35] Add test for strategy-name injection --- freqtrade/tests/optimize/test_backtesting.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index d91781ffc..311fe7da4 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -654,6 +654,18 @@ def test_backtest_record(default_conf, fee, mocker): records = records[0] # Ensure records are of correct type assert len(records) == 4 + + # reset test to test with strategy name + names = [] + records = [] + backtesting._store_backtest_result("backtest-result.json", results, "DefStrat") + assert len(results) == 4 + # Assert file_dump_json was only called once + assert names == ['backtest-result-DefStrat.json'] + records = records[0] + # Ensure records are of correct type + assert len(records) == 4 + # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) # Below follows just a typecheck of the schema/type of trade-records oix = None From 4ea6780153ae4ab1ddab61c4d4ea6eb636a5dc7a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Jul 2018 09:51:45 +0200 Subject: [PATCH 10/35] Update documentation with --strategy-list --- docs/backtesting.md | 19 ++++++++++++++++++- docs/bot-usage.md | 33 ++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 766875970..2d53303c5 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -151,7 +151,7 @@ cp freqtrade/tests/testdata/pairs.json user_data/data/binance Then run: ```bash -python scripts/download_backtest_data --exchange binance +python scripts/download_backtest_data.py --exchange binance ``` This will download ticker data for all the currency pairs you defined in `pairs.json`. @@ -238,6 +238,23 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55` profit. Hence, keep in mind that your performance is a mix of your strategies, your configuration, and the crypto-currency you have set up. +## Backtesting multiple strategies + +To backtest multiple strategies, a list of Strategies can be provided. + +This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple +strategies you'd like to compare, this should give a nice runtime boost. + +All listed Strategies need to be in the same folder. + +``` bash +freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades +``` + +This will save the results to `user_data/backtest_data/backtest-result-.json`, injecting the strategy-name into the target filename. +It will also output all results one after the other, so make sure to scroll up. + + ## Next step Great, your strategy is profitable. What if the bot can give your the diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4e479adac..83a8ee833 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -1,13 +1,15 @@ # Bot usage -This page explains the difference parameters of the bot and how to run -it. + +This page explains the difference parameters of the bot and how to run it. ## Table of Contents + - [Bot commands](#bot-commands) - [Backtesting commands](#backtesting-commands) - [Hyperopt commands](#hyperopt-commands) ## Bot commands + ``` usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]] @@ -41,6 +43,7 @@ optional arguments: ``` ### How to use a different config file? + The bot allows you to select which config file you want to use. Per default, the bot will load the file `./config.json` @@ -49,6 +52,7 @@ python3 ./freqtrade/main.py -c path/far/far/away/config.json ``` ### How to use --strategy? + This parameter will allow you to load your custom strategy class. Per default without `--strategy` or `-s` the bot will load the `DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`). @@ -60,6 +64,7 @@ To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this **Example:** In `user_data/strategies` you have a file `my_awesome_strategy.py` which has a strategy class called `AwesomeStrategy` to load it: + ```bash python3 ./freqtrade/main.py --strategy AwesomeStrategy ``` @@ -70,6 +75,7 @@ message the reason (File not found, or errors in your code). Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md). ### How to use --strategy-path? + This parameter allows you to add an additional strategy lookup path, which gets checked before the default locations (The passed path must be a folder!): ```bash @@ -77,21 +83,25 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/fol ``` #### How to install a strategy? + This is very simple. Copy paste your strategy file into the folder `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it. ### How to use --dynamic-whitelist? + Per default `--dynamic-whitelist` will retrieve the 20 currencies based on BaseVolume. This value can be changed when you run the script. **By Default** Get the 20 currencies based on BaseVolume. + ```bash python3 ./freqtrade/main.py --dynamic-whitelist ``` **Customize the number of currencies to retrieve** Get the 30 currencies based on BaseVolume. + ```bash python3 ./freqtrade/main.py --dynamic-whitelist 30 ``` @@ -102,6 +112,7 @@ negative value (e.g -2), `--dynamic-whitelist` will use the default value (20). ### How to use --db-url? + When you run the bot in Dry-run mode, per default no transactions are stored in a database. If you want to store your bot actions in a DB using `--db-url`. This can also be used to specify a custom database @@ -111,14 +122,14 @@ in production mode. Example command: python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite ``` - ## Backtesting commands Backtesting also uses the config specified via `-c/--config`. ``` -usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp] +usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp] [--timerange TIMERANGE] [-l] [-r] + [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] optional arguments: @@ -139,6 +150,13 @@ optional arguments: refresh the pairs files in tests/testdata with the latest data from the exchange. Use it if you want to run your backtesting with up-to-date data. + --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] + Provide a commaseparated list of strategies to + backtest Please note that ticker-interval needs to be + set either in config or via command line. When using + this together with --export trades, the strategy-name + is injected into the filename (so backtest-data.json + becomes backtest-data-DefaultStrategy.json --export EXPORT export backtest results, argument are: trades Example --export=trades --export-filename PATH @@ -151,6 +169,7 @@ optional arguments: ``` ### How to use --refresh-pairs-cached parameter? + The first time your run Backtesting, it will take the pairs you have set in your config file and download data from Bittrex. @@ -162,7 +181,6 @@ to come back to the previous version.** To test your strategy with latest data, we recommend continuing using the parameter `-l` or `--live`. - ## Hyperopt commands To optimize your strategy, you can use hyperopt parameter hyperoptimization @@ -194,10 +212,11 @@ optional arguments: ``` ## A parameter missing in the configuration? + All parameters for `main.py`, `backtesting`, `hyperopt` are referenced in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84) ## Next step -The optimal strategy of the bot will change with time depending of the -market trends. The next step is to + +The optimal strategy of the bot will change with time depending of the market trends. The next step is to [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md). From 5125076f5d9a809559da3f0717cf553d1fbd6c96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Jul 2018 10:05:16 +0200 Subject: [PATCH 11/35] Fix typo --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 69d48b027..6e242ac1a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -388,7 +388,7 @@ class Backtesting(object): logger.info( '\n' + - ' SELL READON STATS '.center(119, '=') + + ' SELL REASON STATS '.center(119, '=') + '\n%s \n', self._generate_text_table_sell_reason(data, results) From 028589abd273f23cf708fc77430775c6661bdbfe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Jul 2018 13:07:11 +0200 Subject: [PATCH 12/35] Add strategy summary table --- freqtrade/optimize/backtesting.py | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6e242ac1a..6f571ae27 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -157,6 +157,30 @@ class Backtesting(object): tabular_data.append([reason.value, count]) return tabulate(tabular_data, headers=headers, tablefmt="pipe") + def _generate_text_table_strategy(self, all_results: dict) -> str: + """ + Generate summary table per strategy + """ + stake_currency = str(self.config.get('stake_currency')) + + floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f') + tabular_data = [] + headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %', + 'total profit ' + stake_currency, 'avg duration', 'profit', 'loss'] + for strategy, results in all_results.items(): + tabular_data.append([ + strategy, + len(results.index), + results.profit_percent.mean() * 100.0, + results.profit_percent.sum() * 100.0, + results.profit_abs.sum(), + str(timedelta( + minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', + len(results[results.profit_abs > 0]), + len(results[results.profit_abs < 0]) + ]) + return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") + def _store_backtest_result(self, recordfilename: str, results: DataFrame, strategyname: Optional[str] = None) -> None: @@ -404,6 +428,15 @@ class Backtesting(object): ) ) + if len(all_results) > 1: + # Print Strategy summary table + logger.info( + '\n' + + ' Strategy Summary'.center(119, '=') + + '\n%s\n\nFor more details, please look at the detail tables above', + self._generate_text_table_strategy(all_results) + ) + def setup_configuration(args: Namespace) -> Dict[str, Any]: """ From 765d1c769c9552840d8fd6679dd3788b5e5ddd61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Jul 2018 13:07:30 +0200 Subject: [PATCH 13/35] Add test for stratgy summary table --- freqtrade/tests/optimize/test_backtesting.py | 48 ++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 311fe7da4..02f16be85 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -406,6 +406,50 @@ def test_generate_text_table_sell_reason(default_conf, mocker): data={'ETH/BTC': {}}, results=results) == result_str +def test_generate_text_table_strategyn(default_conf, mocker): + """ + Test Backtesting.generate_text_table_sell_reason() method + """ + patch_exchange(mocker) + backtesting = Backtesting(default_conf) + results = {} + results['ETH/BTC'] = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, 0.3], + 'profit_abs': [0.2, 0.4, 0.5], + 'trade_duration': [10, 30, 10], + 'profit': [2, 0, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + results['LTC/BTC'] = pd.DataFrame( + { + 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], + 'profit_percent': [0.4, 0.2, 0.3], + 'profit_abs': [0.4, 0.4, 0.5], + 'trade_duration': [15, 30, 15], + 'profit': [4, 1, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + result_str = ( + '| Strategy | buy count | avg profit % | cum profit % ' + '| total profit BTC | avg duration | profit | loss |\n' + '|:-----------|------------:|---------------:|---------------:' + '|-------------------:|:---------------|---------:|-------:|\n' + '| ETH/BTC | 3 | 20.00 | 60.00 ' + '| 1.10000000 | 0:17:00 | 3 | 0 |\n' + '| LTC/BTC | 3 | 30.00 | 90.00 ' + '| 1.30000000 | 0:20:00 | 3 | 0 |' + ) + print(backtesting._generate_text_table_strategy(all_results=results)) + assert backtesting._generate_text_table_strategy(all_results=results) == result_str + + def test_backtesting_start(default_conf, mocker, caplog) -> None: def get_timeframe(input1, input2): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) @@ -740,6 +784,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) gen_table_mock = MagicMock() mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock) + gen_strattable_mock = MagicMock() + mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', + gen_strattable_mock) mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(conf) )) @@ -762,6 +809,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): # 2 backtests, 4 tables assert backtestmock.call_count == 2 assert gen_table_mock.call_count == 4 + assert gen_strattable_mock.call_count == 1 # check the logs, that will contain the backtest result exists = [ From c648e2acfcad9f3b0af714d3a7105d23d7ffe64a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Jul 2018 13:13:03 +0200 Subject: [PATCH 14/35] Adjust documentation to strategy table --- docs/backtesting.md | 10 +++++++++- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 2d53303c5..cc8ecd6c7 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -252,8 +252,16 @@ freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strat ``` This will save the results to `user_data/backtest_data/backtest-result-.json`, injecting the strategy-name into the target filename. -It will also output all results one after the other, so make sure to scroll up. +There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table). +Detailed output for all strategies one after the other will be available, so make sure to scroll up. +``` +=================================================== Strategy Summary ==================================================== +| Strategy | buy count | avg profit % | cum profit % | total profit ETH | avg duration | profit | loss | +|:-----------|------------:|---------------:|---------------:|-------------------:|:----------------|---------:|-------:| +| Strategy1 | 19 | -0.76 | -14.39 | -0.01440287 | 15:48:00 | 15 | 4 | +| Strategy2 | 6 | -2.73 | -16.40 | -0.01641299 | 1 day, 14:12:00 | 3 | 3 | +``` ## Next step diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6f571ae27..067e7bdca 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -432,7 +432,7 @@ class Backtesting(object): # Print Strategy summary table logger.info( '\n' + - ' Strategy Summary'.center(119, '=') + + ' Strategy Summary '.center(119, '=') + '\n%s\n\nFor more details, please look at the detail tables above', self._generate_text_table_strategy(all_results) ) From 76fbb89a03b84bcb35cfcb585af40d8fcc3e4674 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Jul 2018 19:41:39 +0200 Subject: [PATCH 15/35] use print for backtest results to avoid odd newline-handling --- freqtrade/optimize/backtesting.py | 47 ++++++++----------------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 067e7bdca..53071efaf 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -396,46 +396,21 @@ class Backtesting(object): self._store_backtest_result(self.config['exportfilename'], results, strategy if len(self.strategylist) > 1 else None) - logger.info("\nResult for strategy %s", strategy) - logger.info( - '\n' + - ' BACKTESTING REPORT '.center(119, '=') + - '\n%s', - self._generate_text_table( - data, - results - ) - ) - # logger.info( - # results[['sell_reason']].groupby('sell_reason').count() - # ) + print(f"Result for strategy {strategy}") + print(' BACKTESTING REPORT '.center(119, '=')) + print(self._generate_text_table(data, results)) - logger.info( - '\n' + - ' SELL REASON STATS '.center(119, '=') + - '\n%s \n', - self._generate_text_table_sell_reason(data, results) - - ) - - logger.info( - '\n' + - ' LEFT OPEN TRADES REPORT '.center(119, '=') + - '\n%s', - self._generate_text_table( - data, - results.loc[results.open_at_end] - ) - ) + print(' SELL REASON STATS '.center(119, '=')) + print(self._generate_text_table_sell_reason(data, results)) + print(' LEFT OPEN TRADES REPORT '.center(119, '=')) + print(self._generate_text_table(data, results.loc[results.open_at_end])) + print() if len(all_results) > 1: # Print Strategy summary table - logger.info( - '\n' + - ' Strategy Summary '.center(119, '=') + - '\n%s\n\nFor more details, please look at the detail tables above', - self._generate_text_table_strategy(all_results) - ) + print(' Strategy Summary '.center(119, '=')) + print(self._generate_text_table_strategy(all_results)) + print('\nFor more details, please look at the detail tables above') def setup_configuration(args: Namespace) -> Dict[str, Any]: From 40ee86b3579b35cd69e20766d3e7f1b869d36d86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 31 Jul 2018 21:08:03 +0200 Subject: [PATCH 16/35] Adapt after rebase --- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/tests/optimize/test_backtesting.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 53071efaf..3fd96221b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -91,8 +91,8 @@ class Backtesting(object): self.strategy = strategy self.ticker_interval = self.config.get('ticker_interval') self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe - self.populate_buy_trend = strategy.populate_buy_trend - self.populate_sell_trend = strategy.populate_sell_trend + 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]: diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 02f16be85..0099a3e32 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -775,8 +775,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): def test_backtest_start_multi_strat(default_conf, mocker, caplog): - conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] + default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', new=lambda s, n, i: _load_pair_as_ticks(n, i)) patch_exchange(mocker) @@ -788,7 +787,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog): mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', gen_strattable_mock) mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) + read_data=json.dumps(default_conf) )) args = [ From f619cd1d2aae971098d47f03480c396e027e631a Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 08:45:28 +0000 Subject: [PATCH 17/35] renamed/refactored get_ticker_history to get_candle_history as it does not fetch any ticker data only candles and is causing confusion when developer are talking about candles /tickers incorreclty. OHLCV < candles and Tickers are two seperate datafeeds from the exchange --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/tests/exchange/test_exchange.py | 16 ++++++++-------- freqtrade/tests/test_freqtradebot.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 46fbb3a38..706435017 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -330,7 +330,7 @@ class FreqtradeBot(object): # Pick pair based on buy signals for _pair in whitelist: - thistory = self.exchange.get_ticker_history(_pair, interval) + thistory = self.exchange.get_candle_history(_pair, interval) (buy, sell) = self.strategy.get_signal(_pair, interval, thistory) if buy and not sell: @@ -497,7 +497,7 @@ class FreqtradeBot(object): (buy, sell) = (False, False) experimental = self.config.get('experimental', {}) if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'): - ticker = self.exchange.get_ticker_history(trade.pair, self.strategy.ticker_interval) + ticker = self.exchange.get_candle_history(trade.pair, self.strategy.ticker_interval) (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval, ticker) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index d327b97c7..6918e9da1 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -524,7 +524,7 @@ def make_fetch_ohlcv_mock(data): return fetch_ohlcv_mock -def test_get_ticker_history(default_conf, mocker): +def test_get_candle_history(default_conf, mocker): api_mock = MagicMock() tick = [ [ @@ -541,7 +541,7 @@ def test_get_ticker_history(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock) # retrieve original ticker - ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) + ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) assert ticks[0][0] == 1511686200000 assert ticks[0][1] == 1 assert ticks[0][2] == 2 @@ -563,7 +563,7 @@ def test_get_ticker_history(default_conf, mocker): api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick)) exchange = get_patched_exchange(mocker, default_conf, api_mock) - ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) + ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) assert ticks[0][0] == 1511686210000 assert ticks[0][1] == 6 assert ticks[0][2] == 7 @@ -572,16 +572,16 @@ def test_get_ticker_history(default_conf, mocker): assert ticks[0][5] == 10 ccxt_exceptionhandlers(mocker, default_conf, api_mock, - "get_ticker_history", "fetch_ohlcv", + "get_candle_history", "fetch_ohlcv", pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported) exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.get_ticker_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) + exchange.get_candle_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) -def test_get_ticker_history_sort(default_conf, mocker): +def test_get_candle_history_sort(default_conf, mocker): api_mock = MagicMock() # GDAX use-case (real data from GDAX) @@ -604,7 +604,7 @@ def test_get_ticker_history_sort(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock) # Test the ticker history sort - ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) + ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) assert ticks[0][0] == 1527830400000 assert ticks[0][1] == 0.07649 assert ticks[0][2] == 0.07651 @@ -637,7 +637,7 @@ def test_get_ticker_history_sort(default_conf, mocker): api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) exchange = get_patched_exchange(mocker, default_conf, api_mock) # Test the ticker history sort - ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) + ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) assert ticks[0][0] == 1527827700000 assert ticks[0][1] == 0.07659999 assert ticks[0][2] == 0.0766 diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 69f349107..df73fff3c 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -43,7 +43,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: :return: None """ freqtrade.strategy.get_signal = lambda e, s, t: value - freqtrade.exchange.get_ticker_history = lambda p, i: None + freqtrade.exchange.get_candle_history = lambda p, i: None def patch_RPCManager(mocker) -> MagicMock: From a741f1144a43ec7116718cb5e8128b1ee41b8c77 Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 08:58:04 +0000 Subject: [PATCH 18/35] missing __init__.py --- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/exchange_helpers.py | 2 +- freqtrade/optimize/__init__.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/tests/optimize/test_backtesting.py | 8 ++++---- freqtrade/tests/optimize/test_optimize.py | 16 ++++++++-------- freqtrade/tests/strategy/test_interface.py | 2 +- scripts/plot_dataframe.py | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 423e38246..0be89aaa5 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -330,7 +330,7 @@ class Exchange(object): return self._cached_ticker[pair] @retrier - def get_ticker_history(self, pair: str, tick_interval: str, + def get_candle_history(self, pair: str, tick_interval: str, since_ms: Optional[int] = None) -> List[Dict]: try: # last item should be in the time interval [now - tick_interval, now] diff --git a/freqtrade/exchange/exchange_helpers.py b/freqtrade/exchange/exchange_helpers.py index 254c16309..46f04328c 100644 --- a/freqtrade/exchange/exchange_helpers.py +++ b/freqtrade/exchange/exchange_helpers.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) def parse_ticker_dataframe(ticker: list) -> DataFrame: """ Analyses the trend for the given ticker history - :param ticker: See exchange.get_ticker_history + :param ticker: See exchange.get_candle_history :return: DataFrame """ cols = ['date', 'open', 'high', 'low', 'close', 'volume'] diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index e806ff2b8..8d5350fe5 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -219,7 +219,7 @@ def download_backtesting_testdata(datadir: str, logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') - new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval, + new_data = exchange.get_candle_history(pair=pair, tick_interval=tick_interval, since_ms=since_ms) data.extend(new_data) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 593af619c..fff658b6f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -283,7 +283,7 @@ class Backtesting(object): if self.config.get('live'): logger.info('Downloading data for all pairs in whitelist ...') for pair in pairs: - data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval) + data[pair] = self.exchange.get_candle_history(pair, self.ticker_interval) else: logger.info('Using local backtesting data (using whitelist in given config) ...') diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 5d121d27c..fc7b1f043 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -110,7 +110,7 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals return pairdata -# use for mock freqtrade.exchange.get_ticker_history' +# use for mock freqtrade.exchange.get_candle_history' def _load_pair_as_ticks(pair, tickfreq): ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair]) ticks = trim_dictlist(ticks, -201) @@ -411,7 +411,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: 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.exchange.Exchange.get_ticker_history') + mocker.patch('freqtrade.exchange.Exchange.get_candle_history') patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.optimize.backtesting.Backtesting', @@ -446,7 +446,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: 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.exchange.Exchange.get_ticker_history') + mocker.patch('freqtrade.exchange.Exchange.get_candle_history') patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.optimize.backtesting.Backtesting', @@ -677,7 +677,7 @@ def test_backtest_record(default_conf, fee, mocker): def test_backtest_start_live(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', new=lambda s, n, i: _load_pair_as_ticks(n, i)) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index eef79bef3..13f65fbf5 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -53,7 +53,7 @@ def _clean_test_file(file: str) -> None: def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json') _backup_file(file, copy_file=True) optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m') @@ -63,7 +63,7 @@ def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json') _backup_file(file, copy_file=True) @@ -74,7 +74,7 @@ def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json') _backup_file(file, copy_file=True) optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC']) @@ -87,7 +87,7 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_co """ Test load_data() with 1 min ticker """ - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) exchange = get_patched_exchange(mocker, default_conf) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') @@ -118,7 +118,7 @@ def test_testdata_path() -> None: def test_download_pairs(ticker_history, mocker, default_conf) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) exchange = get_patched_exchange(mocker, default_conf) file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json') @@ -261,7 +261,7 @@ def test_load_cached_data_for_updating(mocker) -> None: def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata', side_effect=BaseException('File Error')) exchange = get_patched_exchange(mocker, default_conf) @@ -279,7 +279,7 @@ def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) exchange = get_patched_exchange(mocker, default_conf) # Download a 1 min ticker file @@ -304,7 +304,7 @@ def test_download_backtesting_testdata2(mocker, default_conf) -> None: [1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199] ] json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=tick) + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m') download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m') diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index 2c056870f..ec4ab0fd4 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -88,7 +88,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog): def test_get_signal_handles_exceptions(mocker, default_conf): - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=MagicMock()) exchange = get_patched_exchange(mocker, default_conf) mocker.patch.object( _STRATEGY, 'analyze_ticker', diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index fbb385a3c..f2f2e0c7f 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -138,7 +138,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None: tickers = {} if args.live: logger.info('Downloading pair.') - tickers[pair] = exchange.get_ticker_history(pair, tick_interval) + tickers[pair] = exchange.get_candle_history(pair, tick_interval) else: tickers = optimize.load_data( datadir=_CONF.get("datadir"), From 1f97d0d78b79b6f4ac889a5ba2c8a2004c5b1111 Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 09:15:02 +0000 Subject: [PATCH 19/35] fix --- freqtrade/tests/test_freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index df73fff3c..89adae6ab 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -544,7 +544,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), - get_ticker_history=MagicMock(return_value=20), + get_candle_history=MagicMock(return_value=20), get_balance=MagicMock(return_value=20), get_fee=fee, ) From e282d57a918513209ebdd41e9359e1e782de7dea Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Thu, 2 Aug 2018 12:57:47 +0300 Subject: [PATCH 20/35] fix broken test --- freqtrade/tests/optimize/test_backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index f492384aa..32a5229c0 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -776,7 +776,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): def test_backtest_start_multi_strat(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', + mocker.patch('freqtrade.exchange.Exchange.get_candle_history', new=lambda s, n, i: _load_pair_as_ticks(n, i)) patch_exchange(mocker) backtestmock = MagicMock() From 7f4472ad7789b846b37f7107b99baca586f25842 Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 10:10:44 +0000 Subject: [PATCH 21/35] As requested in issue #1111 A python script to return - all exchanges supported by CCXT - all markets on a exchange Invoked as `python get_market_pairs.py` it will list exchanges Invoked as `python get_market_pairs binance` it will list all markets on binance --- scripts/get_market_pairs.py | 93 +++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 scripts/get_market_pairs.py diff --git a/scripts/get_market_pairs.py b/scripts/get_market_pairs.py new file mode 100644 index 000000000..6ee6464d3 --- /dev/null +++ b/scripts/get_market_pairs.py @@ -0,0 +1,93 @@ +import os +import sys + +root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(root + '/python') + +import ccxt # noqa: E402 + + +def style(s, style): + return style + s + '\033[0m' + + +def green(s): + return style(s, '\033[92m') + + +def blue(s): + return style(s, '\033[94m') + + +def yellow(s): + return style(s, '\033[93m') + + +def red(s): + return style(s, '\033[91m') + + +def pink(s): + return style(s, '\033[95m') + + +def bold(s): + return style(s, '\033[1m') + + +def underline(s): + return style(s, '\033[4m') + + +def dump(*args): + print(' '.join([str(arg) for arg in args])) + + +def print_supported_exchanges(): + dump('Supported exchanges:', green(', '.join(ccxt.exchanges))) + + +try: + + id = sys.argv[1] # get exchange id from command line arguments + + + # check if the exchange is supported by ccxt + exchange_found = id in ccxt.exchanges + + if exchange_found: + dump('Instantiating', green(id), 'exchange') + + # instantiate the exchange by id + exchange = getattr(ccxt, id)({ + # 'proxy':'https://cors-anywhere.herokuapp.com/', + }) + + # load all markets from the exchange + markets = exchange.load_markets() + + # output a list of all market symbols + dump(green(id), 'has', len(exchange.symbols), 'symbols:', exchange.symbols) + + tuples = list(ccxt.Exchange.keysort(markets).items()) + + # debug + for (k, v) in tuples: + print(v) + + # output a table of all markets + dump(pink('{:<15} {:<15} {:<15} {:<15}'.format('id', 'symbol', 'base', 'quote'))) + + for (k, v) in tuples: + dump('{:<15} {:<15} {:<15} {:<15}'.format(v['id'], v['symbol'], v['base'], v['quote'])) + + else: + + dump('Exchange ' + red(id) + ' not found') + print_supported_exchanges() + +except Exception as e: + dump('[' + type(e).__name__ + ']', str(e)) + dump("Usage: python " + sys.argv[0], green('id')) + print_supported_exchanges() + From 0fc4a7910d01f79491d97b1a37d52cb4cd24c72e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Aug 2018 20:15:18 +0200 Subject: [PATCH 22/35] Add note to readme for binance users --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da691230f..7b6b4996b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ hesitate to read the source code and understand the mechanism of this bot. ## Exchange marketplaces supported - [X] [Bittrex](https://bittrex.com/) -- [X] [Binance](https://www.binance.com/) +- [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance)) - [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ## Features @@ -152,6 +152,13 @@ The project is currently setup in two main branches: - `develop` - This branch has often new features, but might also cause breaking changes. - `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. +- `feat/*` - This are feature branches, which are beeing worked on heavily. Please don't use these unless you want to test a specific feature. + + +## A note on Binance + +For Binance, please add `"BNB/"` to your blacklist to avoid issues. +Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore. ## Support From 00b81e3f0df781c2e718b94b23dff3e467a7e4dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Aug 2018 11:45:28 +0200 Subject: [PATCH 23/35] fix readme.md spelling --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b6b4996b..02b870209 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ The project is currently setup in two main branches: - `develop` - This branch has often new features, but might also cause breaking changes. - `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. -- `feat/*` - This are feature branches, which are beeing worked on heavily. Please don't use these unless you want to test a specific feature. +- `feat/*` - These are feature branches, which are beeing worked on heavily. Please don't use these unless you want to test a specific feature. ## A note on Binance From 79b5125a3bb7b01d51b27e43fb2c49d2b10fce9b Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 12:04:13 +0000 Subject: [PATCH 24/35] Do not run indicators on candle rows already processed. track the last candle processed by indicators and buy/sell logic do not pass to them for expensive processing if already processed. current flow is we throw away incomplete candle and then pass the same dataframe to be analysed for any change This change will reduce CPU on host system between candles as opposed to constantly burning a loop it will also allow faster loop processing. --- freqtrade/strategy/interface.py | 38 ++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index dfd624393..50be4c16b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -19,6 +19,23 @@ from freqtrade.persistence import Trade logger = logging.getLogger(__name__) +class candle_analyzed: + ''' + Maintains candle_row, an int set by analyze_ticker + this allows analyze_ticker to not keep testing the same candle data + which is wastful in CPU and time + ''' + def __init__(self, candle_row = 0): + self.candle_row = candle_row + + def get_candle_row(self): + return self._candle_row + + def set_candle_row(self, row): + self._candle_row = row + + candle_row = property(get_candle_row, set_candle_row) + class SignalType(Enum): """ Enum to distinguish between buy and sell signals @@ -72,6 +89,7 @@ class IStrategy(ABC): def __init__(self, config: dict) -> None: self.config = config + self.r = candle_analyzed() @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -112,10 +130,24 @@ class IStrategy(ABC): add several TA indicators and buy signal to it :return DataFrame with ticker data and indicator data """ + + # Get last candle processed and ln of Dataframe + last_candle_processed = self.r.get_candle_row() dataframe = parse_ticker_dataframe(ticker_history) - dataframe = self.advise_indicators(dataframe, metadata) - dataframe = self.advise_buy(dataframe, metadata) - dataframe = self.advise_sell(dataframe, metadata) + + if last_candle_processed != len(dataframe.index): + # Defs that only make change on new candle data here + dataframe = self.advise_indicators(dataframe, metadata) + dataframe = self.advise_buy(dataframe, metadata) + dataframe = self.advise_sell(dataframe, metadata) + self.r.set_candle_row(len(dataframe.index)) + else: + dataframe.loc['buy'] = 0 + dataframe.loc['sell'] = 0 + + ## Other Defs that want to see and run every ticker here: + # example = self.watch_ticker(do something) + return dataframe def get_signal(self, pair: str, interval: str, ticker_hist: List[Dict]) -> Tuple[bool, bool]: From 9070cd1206afd0ccf993ee9f245748e8054edecd Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 12:14:40 +0000 Subject: [PATCH 25/35] flake 8 --- freqtrade/strategy/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 50be4c16b..90bb84f0b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -36,6 +36,7 @@ class candle_analyzed: candle_row = property(get_candle_row, set_candle_row) + class SignalType(Enum): """ Enum to distinguish between buy and sell signals @@ -145,7 +146,7 @@ class IStrategy(ABC): dataframe.loc['buy'] = 0 dataframe.loc['sell'] = 0 - ## Other Defs that want to see and run every ticker here: + # Other Defs that want to see and run every ticker here: # example = self.watch_ticker(do something) return dataframe From a1d854fd904abd53ad316a62771c59999122636c Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 12:21:50 +0000 Subject: [PATCH 26/35] flake 8 --- freqtrade/strategy/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 90bb84f0b..ff370ee2b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -146,8 +146,8 @@ class IStrategy(ABC): dataframe.loc['buy'] = 0 dataframe.loc['sell'] = 0 - # Other Defs that want to see and run every ticker here: - # example = self.watch_ticker(do something) + # Other Defs in startegy that want to be called every loop here + # twitter_sell = self.watch_twitter_feed(dataframe, metadata) return dataframe From 88c9aa68968aacfda954fffcd51423a2d0163164 Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 12:38:25 +0000 Subject: [PATCH 27/35] flake 8 --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index ff370ee2b..45516552a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -25,7 +25,7 @@ class candle_analyzed: this allows analyze_ticker to not keep testing the same candle data which is wastful in CPU and time ''' - def __init__(self, candle_row = 0): + def __init__(self, candle_row=0): self.candle_row = candle_row def get_candle_row(self): From c952bfa991291432f31138ae8fb8cf794d16f72d Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 12:41:07 +0000 Subject: [PATCH 28/35] typo in comments --- freqtrade/strategy/interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 45516552a..1e41ec531 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -22,8 +22,8 @@ logger = logging.getLogger(__name__) class candle_analyzed: ''' Maintains candle_row, an int set by analyze_ticker - this allows analyze_ticker to not keep testing the same candle data - which is wastful in CPU and time + This allows analyze_ticker to test if analysed the candle row in dataframe prior. + To not keep testing the same candle data, which is wasteful in CPU and time ''' def __init__(self, candle_row=0): self.candle_row = candle_row @@ -146,7 +146,7 @@ class IStrategy(ABC): dataframe.loc['buy'] = 0 dataframe.loc['sell'] = 0 - # Other Defs in startegy that want to be called every loop here + # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) return dataframe From 2f5d9dc7525277a59676764b865c52af6679d619 Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 12:51:53 +0000 Subject: [PATCH 29/35] typo in comments --- freqtrade/strategy/interface.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1e41ec531..522e205e7 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -19,9 +19,9 @@ from freqtrade.persistence import Trade logger = logging.getLogger(__name__) -class candle_analyzed: +class CandleAnalyzed: ''' - Maintains candle_row, an int set by analyze_ticker + Maintains candle_row, the last df ['date'], set by analyze_ticker This allows analyze_ticker to test if analysed the candle row in dataframe prior. To not keep testing the same candle data, which is wasteful in CPU and time ''' @@ -90,7 +90,7 @@ class IStrategy(ABC): def __init__(self, config: dict) -> None: self.config = config - self.r = candle_analyzed() + self.r = CandleAnalyzed() @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -136,12 +136,12 @@ class IStrategy(ABC): last_candle_processed = self.r.get_candle_row() dataframe = parse_ticker_dataframe(ticker_history) - if last_candle_processed != len(dataframe.index): + if last_candle_processed != dataframe.iloc[-1]['date']: # Defs that only make change on new candle data here dataframe = self.advise_indicators(dataframe, metadata) dataframe = self.advise_buy(dataframe, metadata) dataframe = self.advise_sell(dataframe, metadata) - self.r.set_candle_row(len(dataframe.index)) + self.r.set_candle_row(dataframe.iloc[-1]['date']) else: dataframe.loc['buy'] = 0 dataframe.loc['sell'] = 0 From 853d67d8d905cb8a6d6b0f1a8ae5eb7cb5d89a1e Mon Sep 17 00:00:00 2001 From: creslin Date: Thu, 2 Aug 2018 12:54:45 +0000 Subject: [PATCH 30/35] Changed class name to follow CaseConvention Changed to test to check Date string from last row in DF. Test rolling over a 5min timeframe, with print inside the it block watched it be processed firt loop after the new candle then not after. --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 522e205e7..10a7c5f6b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) class CandleAnalyzed: ''' - Maintains candle_row, the last df ['date'], set by analyze_ticker + Maintains candle_row, the last df ['date'], set by analyze_ticker. This allows analyze_ticker to test if analysed the candle row in dataframe prior. To not keep testing the same candle data, which is wasteful in CPU and time ''' From 2839ecc9bcd5815aca8cbc2c3789806949274a0d Mon Sep 17 00:00:00 2001 From: creslin Date: Fri, 3 Aug 2018 06:57:53 +0000 Subject: [PATCH 31/35] Test if ran candle analysis per pair/last row --- freqtrade/strategy/interface.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 10a7c5f6b..d9919a565 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -132,22 +132,28 @@ class IStrategy(ABC): :return DataFrame with ticker data and indicator data """ - # Get last candle processed and ln of Dataframe - last_candle_processed = self.r.get_candle_row() + # Test if seen this pair and last candle before. dataframe = parse_ticker_dataframe(ticker_history) - if last_candle_processed != dataframe.iloc[-1]['date']: + last_seen = metadata['pair'] + str(dataframe.iloc[-1]['date']) + last_candle_processed = self.r.get_candle_row() + + if last_candle_processed != last_seen: # Defs that only make change on new candle data here + logging.info("New Candle Analysis Launched") dataframe = self.advise_indicators(dataframe, metadata) dataframe = self.advise_buy(dataframe, metadata) dataframe = self.advise_sell(dataframe, metadata) - self.r.set_candle_row(dataframe.iloc[-1]['date']) + + last_seen = metadata['pair'] + str(dataframe.iloc[-1]['date']) + self.r.set_candle_row(last_seen) else: dataframe.loc['buy'] = 0 dataframe.loc['sell'] = 0 # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) + logging.info("Loop Analysis Launched") return dataframe From 354401bb1ee234533431df1d50bbcff0a28bbeeb Mon Sep 17 00:00:00 2001 From: creslin Date: Fri, 3 Aug 2018 07:13:37 +0000 Subject: [PATCH 32/35] Made ta_on_candle and optional No change to existing bot behaviour. IF a user sets `ta_on_candle": true,` in their config.json then only when new candle-for-pair is recieved are TA functions called. --- freqtrade/constants.py | 1 + freqtrade/strategy/interface.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 87e354455..189b78617 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -53,6 +53,7 @@ CONF_SCHEMA = { }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, + 'ta_on_candle': {'type': 'boolean'}, 'minimal_roi': { 'type': 'object', 'patternProperties': { diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d9919a565..54bd4d816 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -138,9 +138,9 @@ class IStrategy(ABC): last_seen = metadata['pair'] + str(dataframe.iloc[-1]['date']) last_candle_processed = self.r.get_candle_row() - if last_candle_processed != last_seen: - # Defs that only make change on new candle data here - logging.info("New Candle Analysis Launched") + if last_candle_processed != last_seen or self.config.get('ta_on_candle') == False: + # Defs that only make change on new candle data. + logging.info("TA Analysis Launched") dataframe = self.advise_indicators(dataframe, metadata) dataframe = self.advise_buy(dataframe, metadata) dataframe = self.advise_sell(dataframe, metadata) From e35daf787ac0f88669594e9eb3e27700757f1f9f Mon Sep 17 00:00:00 2001 From: creslin Date: Fri, 3 Aug 2018 07:17:27 +0000 Subject: [PATCH 33/35] Updated configuration.md with new optional flag --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index ff5ce118c..b38229d33 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -23,6 +23,7 @@ The table below will list all configuration parameters. | `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes | `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below. | `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. +| `ta_on_candle` | false | No | if set to true indicators are processed each new candle. If false each bot loop, the may mean the same candle is processed many times creating system load and buy/sell signals. | `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. | `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). From 555e3444fb394181f8b8b3fe0d7257cf2f9181b5 Mon Sep 17 00:00:00 2001 From: creslin Date: Fri, 3 Aug 2018 07:18:45 +0000 Subject: [PATCH 34/35] typo --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index b38229d33..6f03117ba 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -23,7 +23,7 @@ The table below will list all configuration parameters. | `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes | `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below. | `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. -| `ta_on_candle` | false | No | if set to true indicators are processed each new candle. If false each bot loop, the may mean the same candle is processed many times creating system load and buy/sell signals. +| `ta_on_candle` | false | No | if set to true indicators are processed each new candle. If false each bot loop, this will mean the same candle is processed many times creating system load and buy/sell signals. | `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. | `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). From f8014da5be4cf0e5fc45962ac1f5d129785ba641 Mon Sep 17 00:00:00 2001 From: creslin Date: Fri, 3 Aug 2018 07:21:05 +0000 Subject: [PATCH 35/35] flake 8 --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 54bd4d816..e68d484de 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -138,7 +138,7 @@ class IStrategy(ABC): last_seen = metadata['pair'] + str(dataframe.iloc[-1]['date']) last_candle_processed = self.r.get_candle_row() - if last_candle_processed != last_seen or self.config.get('ta_on_candle') == False: + if last_candle_processed != last_seen or self.config.get('ta_on_candle') is False: # Defs that only make change on new candle data. logging.info("TA Analysis Launched") dataframe = self.advise_indicators(dataframe, metadata)