From 2f225e23408bc6c47118840bfddfb829ba6d6776 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 19 Feb 2019 15:14:47 +0300 Subject: [PATCH 01/46] multiple --config options --- freqtrade/arguments.py | 76 ++++++++++++++++++++------------------ freqtrade/configuration.py | 22 +++++++---- freqtrade/constants.py | 1 + freqtrade/misc.py | 18 +++++++++ 4 files changed, 74 insertions(+), 43 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 9b1b9a925..53a46d141 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -6,9 +6,7 @@ import argparse import os import re from typing import List, NamedTuple, Optional - import arrow - from freqtrade import __version__, constants @@ -55,6 +53,11 @@ class Arguments(object): """ parsed_arg = self.parser.parse_args(self.args) + # Workaround issue in argparse with action='append' and default value + # (see https://bugs.python.org/issue16399) + if parsed_arg.config is None: + parsed_arg.config = [constants.DEFAULT_CONFIG] + return parsed_arg def common_args_parser(self) -> None: @@ -63,7 +66,7 @@ class Arguments(object): """ self.parser.add_argument( '-v', '--verbose', - help='verbose mode (-vv for more, -vvv to get all messages)', + help='Verbose mode (-vv for more, -vvv to get all messages).', action='count', dest='loglevel', default=0, @@ -75,15 +78,15 @@ class Arguments(object): ) self.parser.add_argument( '-c', '--config', - help='specify configuration file (default: %(default)s)', + help='Specify configuration file (default: %(default)s).', dest='config', - default='config.json', + action='append', type=str, metavar='PATH', ) self.parser.add_argument( '-d', '--datadir', - help='path to backtest data', + help='Path to backtest data.', dest='datadir', default=None, type=str, @@ -91,7 +94,7 @@ class Arguments(object): ) self.parser.add_argument( '-s', '--strategy', - help='specify strategy class name (default: %(default)s)', + help='Specify strategy class name (default: %(default)s).', dest='strategy', default='DefaultStrategy', type=str, @@ -99,14 +102,14 @@ class Arguments(object): ) self.parser.add_argument( '--strategy-path', - help='specify additional strategy lookup path', + help='Specify additional strategy lookup path.', dest='strategy_path', type=str, metavar='PATH', ) self.parser.add_argument( '--customhyperopt', - help='specify hyperopt class name (default: %(default)s)', + help='Specify hyperopt class name (default: %(default)s).', dest='hyperopt', default=constants.DEFAULT_HYPEROPT, type=str, @@ -114,8 +117,8 @@ class Arguments(object): ) self.parser.add_argument( '--dynamic-whitelist', - help='dynamically generate and update whitelist' - ' based on 24h BaseVolume (default: %(const)s)' + help='Dynamically generate and update whitelist' + ' based on 24h BaseVolume (default: %(const)s).' ' DEPRECATED.', dest='dynamic_whitelist', const=constants.DYNAMIC_WHITELIST, @@ -126,7 +129,7 @@ class Arguments(object): self.parser.add_argument( '--db-url', help='Override trades database URL, this is useful if dry_run is enabled' - ' or in custom deployments (default: %(default)s)', + ' or in custom deployments (default: %(default)s).', dest='db_url', type=str, metavar='PATH', @@ -139,7 +142,7 @@ class Arguments(object): """ parser.add_argument( '--eps', '--enable-position-stacking', - help='Allow buying the same pair multiple times (position stacking)', + help='Allow buying the same pair multiple times (position stacking).', action='store_true', dest='position_stacking', default=False @@ -148,20 +151,20 @@ class Arguments(object): parser.add_argument( '--dmmp', '--disable-max-market-positions', help='Disable applying `max_open_trades` during backtest ' - '(same as setting `max_open_trades` to a very high number)', + '(same as setting `max_open_trades` to a very high number).', action='store_false', dest='use_max_market_positions', default=True ) parser.add_argument( '-l', '--live', - help='using live data', + help='Use live data.', action='store_true', dest='live', ) parser.add_argument( '-r', '--refresh-pairs-cached', - help='refresh the pairs files in tests/testdata with the latest data from the ' + help='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.', action='store_true', dest='refresh_pairs', @@ -178,8 +181,8 @@ class Arguments(object): ) parser.add_argument( '--export', - help='export backtest results, argument are: trades\ - Example --export=trades', + help='Export backtest results, argument are: trades. ' + 'Example --export=trades', type=str, default=None, dest='export', @@ -203,14 +206,14 @@ class Arguments(object): """ parser.add_argument( '-r', '--refresh-pairs-cached', - help='refresh the pairs files in tests/testdata with the latest data from the ' + help='Refresh the pairs files in tests/testdata with the latest data from the ' 'exchange. Use it if you want to run your edge with up-to-date data.', action='store_true', dest='refresh_pairs', ) parser.add_argument( '--stoplosses', - help='defines a range of stoploss against which edge will assess the strategy ' + help='Defines a range of stoploss against which edge will assess the strategy ' 'the format is "min,max,step" (without any space).' 'example: --stoplosses=-0.01,-0.1,-0.001', type=str, @@ -226,14 +229,14 @@ class Arguments(object): """ parser.add_argument( '-i', '--ticker-interval', - help='specify ticker interval (1m, 5m, 30m, 1h, 1d)', + help='Specify ticker interval (1m, 5m, 30m, 1h, 1d).', dest='ticker_interval', type=str, ) parser.add_argument( '--timerange', - help='specify what timerange of data to use.', + help='Specify what timerange of data to use.', default=None, type=str, dest='timerange', @@ -246,7 +249,7 @@ class Arguments(object): """ parser.add_argument( '--eps', '--enable-position-stacking', - help='Allow buying the same pair multiple times (position stacking)', + help='Allow buying the same pair multiple times (position stacking).', action='store_true', dest='position_stacking', default=False @@ -255,14 +258,14 @@ class Arguments(object): parser.add_argument( '--dmmp', '--disable-max-market-positions', help='Disable applying `max_open_trades` during backtest ' - '(same as setting `max_open_trades` to a very high number)', + '(same as setting `max_open_trades` to a very high number).', action='store_false', dest='use_max_market_positions', default=True ) parser.add_argument( '-e', '--epochs', - help='specify number of epochs (default: %(default)d)', + help='Specify number of epochs (default: %(default)d).', dest='epochs', default=constants.HYPEROPT_EPOCH, type=int, @@ -271,7 +274,7 @@ class Arguments(object): parser.add_argument( '-s', '--spaces', help='Specify which parameters to hyperopt. Space separate list. \ - Default: %(default)s', + Default: %(default)s.', choices=['all', 'buy', 'sell', 'roi', 'stoploss'], default='all', nargs='+', @@ -288,19 +291,19 @@ class Arguments(object): subparsers = self.parser.add_subparsers(dest='subparser') # Add backtesting subcommand - backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module') + backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') backtesting_cmd.set_defaults(func=backtesting.start) self.optimizer_shared_options(backtesting_cmd) self.backtesting_options(backtesting_cmd) # Add edge subcommand - edge_cmd = subparsers.add_parser('edge', help='edge module') + edge_cmd = subparsers.add_parser('edge', help='Edge module.') edge_cmd.set_defaults(func=edge_cli.start) self.optimizer_shared_options(edge_cmd) self.edge_options(edge_cmd) # Add hyperopt subcommand - hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') + hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') hyperopt_cmd.set_defaults(func=hyperopt.start) self.optimizer_shared_options(hyperopt_cmd) self.hyperopt_options(hyperopt_cmd) @@ -364,7 +367,7 @@ class Arguments(object): """ self.parser.add_argument( '--pairs-file', - help='File containing a list of pairs to download', + help='File containing a list of pairs to download.', dest='pairs_file', default=None, metavar='PATH', @@ -372,7 +375,7 @@ class Arguments(object): self.parser.add_argument( '--export', - help='Export files to given dir', + help='Export files to given dir.', dest='export', default=None, metavar='PATH', @@ -380,7 +383,8 @@ class Arguments(object): self.parser.add_argument( '-c', '--config', - help='specify configuration file, used for additional exchange parameters', + help='Specify configuration file, used for additional exchange parameters. ' + 'Multiple --config options may be used.', dest='config', default=None, type=str, @@ -389,7 +393,7 @@ class Arguments(object): self.parser.add_argument( '--days', - help='Download data for number of days', + help='Download data for given number of days.', dest='days', type=int, metavar='INT', @@ -398,7 +402,7 @@ class Arguments(object): self.parser.add_argument( '--exchange', - help='Exchange name (default: %(default)s). Only valid if no config is provided', + help='Exchange name (default: %(default)s). Only valid if no config is provided.', dest='exchange', type=str, default='bittrex' @@ -407,7 +411,7 @@ class Arguments(object): self.parser.add_argument( '-t', '--timeframes', help='Specify which tickers to download. Space separated list. \ - Default: %(default)s', + Default: %(default)s.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w'], default=['1m', '5m'], @@ -417,7 +421,7 @@ class Arguments(object): self.parser.add_argument( '--erase', - help='Clean all existing data for the selected exchange/pairs/timeframes', + help='Clean all existing data for the selected exchange/pairs/timeframes.', dest='erase', action='store_true' ) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index d972f50b8..bddf60028 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -13,6 +13,8 @@ from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants from freqtrade.state import RunMode +from freqtrade.misc import deep_merge_dicts + logger = logging.getLogger(__name__) @@ -45,8 +47,18 @@ class Configuration(object): Extract information for sys.argv and load the bot configuration :return: Configuration dictionary """ - logger.info('Using config: %s ...', self.args.config) - config = self._load_config_file(self.args.config) + config: Dict[str, Any] = {} + # Now expecting a list of config filenames here, not a string + for path in self.args.config: + logger.info('Using config: %s ...', path) + # Merge config options, overwriting old values + config = deep_merge_dicts(self._load_config_file(path), config) + + if 'internals' not in config: + config['internals'] = {} + + logger.info('Validating configuration ...') + self._validate_config(config) # Set strategy if not specified in config and or if it's non default if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): @@ -93,11 +105,7 @@ class Configuration(object): f'Config file "{path}" not found!' ' Please create a config file or check whether it exists.') - if 'internals' not in conf: - conf['internals'] = {} - logger.info('Validating configuration ...') - - return self._validate_config(conf) + return conf def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """ diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 8fbcdfed7..c214f0de9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -3,6 +3,7 @@ """ bot constants """ +DEFAULT_CONFIG = 'config.json' DYNAMIC_WHITELIST = 20 # pairs PROCESS_THROTTLE_SECS = 5 # sec TICKER_INTERVAL = 5 # min diff --git a/freqtrade/misc.py b/freqtrade/misc.py index d03187d77..38f758669 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -113,3 +113,21 @@ def format_ms_time(date: int) -> str: : epoch-string in ms """ return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S') + + +def deep_merge_dicts(source, destination): + """ + >>> a = { 'first' : { 'rows' : { 'pass' : 'dog', 'number' : '1' } } } + >>> b = { 'first' : { 'rows' : { 'fail' : 'cat', 'number' : '5' } } } + >>> merge(b, a) == { 'first' : { 'rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } } + True + """ + for key, value in source.items(): + if isinstance(value, dict): + # get node or create one + node = destination.setdefault(key, {}) + deep_merge_dicts(value, node) + else: + destination[key] = value + + return destination From e495ffec78a0aa0d666863cbd81e06dd988478d3 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Wed, 20 Feb 2019 02:38:16 +0100 Subject: [PATCH 02/46] align dry_run_orders --- freqtrade/exchange/__init__.py | 68 +++++++++++++++------------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 2b6d13fcf..078c8e6be 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -283,21 +283,32 @@ class Exchange(object): price = ceil(big_price) / pow(10, symbol_prec) return price + def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, + rate: float, params: Dict = {}) -> Dict: + order_id = f'dry_run_buy_{randint(0, 10**6)}' + dry_order = { + "id": order_id, + 'pair': pair, + 'price': rate, + 'amount': amount, + 'type': ordertype, + 'side': 'buy', + 'remaining': 0.0, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'closed', + 'fee': None # should this be None or skipped? + } + return order_id, dry_order + + def create_order(self, pair: str, ordertype: str, side: str, amount: float, + rate: float, params: Dict = {}) -> Dict: + pass # TODO: finish this + def buy(self, pair: str, ordertype: str, amount: float, rate: float, time_in_force) -> Dict: if self._conf['dry_run']: - order_id = f'dry_run_buy_{randint(0, 10**6)}' - self._dry_run_open_orders[order_id] = { - 'pair': pair, - 'price': rate, - 'amount': amount, - 'type': ordertype, - 'side': 'buy', - 'remaining': 0.0, - 'datetime': arrow.utcnow().isoformat(), - 'status': 'closed', - 'fee': None - } + order_id, dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate) + self._dry_run_open_orders[order_id] = dry_order return {'id': order_id} try: @@ -331,17 +342,8 @@ class Exchange(object): def sell(self, pair: str, ordertype: str, amount: float, rate: float, time_in_force='gtc') -> Dict: if self._conf['dry_run']: - order_id = f'dry_run_sell_{randint(0, 10**6)}' - self._dry_run_open_orders[order_id] = { - 'pair': pair, - 'price': rate, - 'amount': amount, - 'type': ordertype, - 'side': 'sell', - 'remaining': 0.0, - 'datetime': arrow.utcnow().isoformat(), - 'status': 'closed' - } + order_id, dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate) + self._dry_run_open_orders[order_id] = dry_order return {'id': order_id} try: @@ -389,21 +391,11 @@ class Exchange(object): 'In stoploss limit order, stop price should be more than limit price') if self._conf['dry_run']: - order_id = f'dry_run_buy_{randint(0, 10**6)}' - self._dry_run_open_orders[order_id] = { - 'info': {}, - 'id': order_id, - 'pair': pair, - 'price': stop_price, - 'amount': amount, - 'type': 'stop_loss_limit', - 'side': 'sell', - 'remaining': amount, - 'datetime': arrow.utcnow().isoformat(), - 'status': 'open', - 'fee': None - } - return self._dry_run_open_orders[order_id] + order_id, dry_order = self.dry_run_order( + pair, "stop_loss_limit", "sell", amount, stop_price, rate) + dry_order.update({"info": {}, "remaining": amount, "status": "open"}) + self._dry_run_open_orders[order_id] = dry_order + return dry_order try: From 5906d37818d4039f7f9452d9574846443b7e8fa1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 20 Feb 2019 15:12:04 +0300 Subject: [PATCH 03/46] code cleanup in _process() --- freqtrade/freqtradebot.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f67be724c..e12456cf8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,7 +7,7 @@ import logging import time import traceback from datetime import datetime -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, Tuple import arrow from requests.exceptions import RequestException @@ -166,15 +166,11 @@ class FreqtradeBot(object): 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]) + # It ensures that tickers are downloaded for open trades + self._extend_whitelist_with_trades(self.active_pair_whitelist) - # Create pair-whitelist tuple with (pair, ticker_interval) - pair_whitelist_tuple = [(pair, self.config['ticker_interval']) - for pair in self.active_pair_whitelist] # Refreshing candles - self.dataprovider.refresh(pair_whitelist_tuple, + self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), self.strategy.informative_pairs()) # First process current opened trades @@ -204,6 +200,19 @@ class FreqtradeBot(object): self.state = State.STOPPED return state_changed + def _extend_whitelist_with_trades(self, whitelist: List[str]): + # Query trades from persistence layer + trades = Trade.query.filter(Trade.is_open.is_(True)).all() + + # Extend whitelist with pairs from open trades + whitelist.extend([trade.pair for trade in trades if trade.pair not in whitelist]) + + def _create_pair_whitelist(self, pairs: List[str]) -> List[Tuple[str, str]]: + """ + Create pair-whitelist tuple with (pair, ticker_interval) + """ + return [(pair, self.config['ticker_interval']) for pair in pairs] + def get_target_bid(self, pair: str) -> float: """ Calculates bid target between current ask price and last price From 199e3d2234b6c7d3361bff5f8b5fd1920e8db6ac Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 20 Feb 2019 15:13:21 +0300 Subject: [PATCH 04/46] typo in a comment --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e12456cf8..eda648a64 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -157,7 +157,7 @@ class FreqtradeBot(object): self.pairlists.refresh_pairlist() self.active_pair_whitelist = self.pairlists.whitelist - # Calculating Edge positiong + # Calculating Edge positioning if self.edge: self.edge.calculate() self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist) From fac0e4e603109856c7ca877f0e75f59570cece35 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 20 Feb 2019 15:56:26 +0300 Subject: [PATCH 05/46] more code cleanup in _process() --- freqtrade/freqtradebot.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index eda648a64..6f884a98f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -163,11 +163,11 @@ class FreqtradeBot(object): self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist) # Query trades from persistence layer - trades = Trade.query.filter(Trade.is_open.is_(True)).all() + trades = self._query_trades() # Extend active-pair whitelist with pairs from open trades # It ensures that tickers are downloaded for open trades - self._extend_whitelist_with_trades(self.active_pair_whitelist) + self._extend_whitelist_with_trades(self.active_pair_whitelist, trades) # Refreshing candles self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), @@ -200,10 +200,11 @@ class FreqtradeBot(object): self.state = State.STOPPED return state_changed - def _extend_whitelist_with_trades(self, whitelist: List[str]): + def _query_trades(self) -> List[Any]: # Query trades from persistence layer - trades = Trade.query.filter(Trade.is_open.is_(True)).all() + return Trade.query.filter(Trade.is_open.is_(True)).all() + def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): # Extend whitelist with pairs from open trades whitelist.extend([trade.pair for trade in trades if trade.pair not in whitelist]) From 7bc874c7fd7e0e750b1a16968875f88cf21f4d55 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 20 Feb 2019 16:12:17 +0300 Subject: [PATCH 06/46] comments adjusted --- freqtrade/freqtradebot.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6f884a98f..295c204e3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -201,11 +201,15 @@ class FreqtradeBot(object): return state_changed def _query_trades(self) -> List[Any]: - # Query trades from persistence layer + """ + Query trades from persistence layer + """ return Trade.query.filter(Trade.is_open.is_(True)).all() def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): - # Extend whitelist with pairs from open trades + """ + Extend whitelist with pairs from open trades + """ whitelist.extend([trade.pair for trade in trades if trade.pair not in whitelist]) def _create_pair_whitelist(self, pairs: List[str]) -> List[Tuple[str, str]]: From c08a2b6638f735c0f188b2f94d59f539d714cf0d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 20 Feb 2019 16:23:09 +0300 Subject: [PATCH 07/46] help message fixed --- freqtrade/arguments.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 53a46d141..f462926f9 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -78,7 +78,8 @@ class Arguments(object): ) self.parser.add_argument( '-c', '--config', - help='Specify configuration file (default: %(default)s).', + help='Specify configuration file (default: %(default)s). ' + 'Multiple --config options may be used.', dest='config', action='append', type=str, From 87c82dea3d93a8321a6594e024e48ad3b30c5b17 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 20 Feb 2019 17:00:35 +0300 Subject: [PATCH 08/46] support for multiple --config in the download_backtest_data.py utility --- freqtrade/arguments.py | 4 ++-- scripts/download_backtest_data.py | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index f462926f9..62f22befc 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -384,10 +384,10 @@ class Arguments(object): self.parser.add_argument( '-c', '--config', - help='Specify configuration file, used for additional exchange parameters. ' + help='Specify configuration file (default: %(default)s). ' 'Multiple --config options may be used.', dest='config', - default=None, + action='append', type=str, metavar='PATH', ) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index c8fd08747..df3e4bb0c 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -5,12 +5,14 @@ import json import sys from pathlib import Path import arrow +from typing import Any, Dict -from freqtrade import arguments +from freqtrade.arguments import Arguments from freqtrade.arguments import TimeRange from freqtrade.exchange import Exchange from freqtrade.data.history import download_pair_history from freqtrade.configuration import Configuration, set_loggers +from freqtrade.misc import deep_merge_dicts import logging logging.basicConfig( @@ -21,7 +23,7 @@ set_loggers(0) DEFAULT_DL_PATH = 'user_data/data' -arguments = arguments.Arguments(sys.argv[1:], 'download utility') +arguments = Arguments(sys.argv[1:], 'download utility') arguments.testdata_dl_options() args = arguments.parse_args() @@ -29,7 +31,15 @@ timeframes = args.timeframes if args.config: configuration = Configuration(args) - config = configuration._load_config_file(args.config) + + config: Dict[str, Any] = {} + # Now expecting a list of config filenames here, not a string + for path in args.config: + print('Using config: %s ...', path) + # Merge config options, overwriting old values + config = deep_merge_dicts(configuration._load_config_file(path), config) + +### config = configuration._load_config_file(args.config) config['stake_currency'] = '' # Ensure we do not use Exchange credentials From 4fbba98168dfaca60733b055dc21e10f22ea5b12 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 20 Feb 2019 17:54:20 +0300 Subject: [PATCH 09/46] tests adjusted for multiple --config options --- freqtrade/tests/test_arguments.py | 15 ++++++++++----- freqtrade/tests/test_configuration.py | 12 +++++++----- freqtrade/tests/test_main.py | 4 ++-- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 042d43ed2..0952d1c5d 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -16,7 +16,7 @@ def test_parse_args_none() -> None: def test_parse_args_defaults() -> None: args = Arguments([], '').get_parsed_arg() - assert args.config == 'config.json' + assert args.config == ['config.json'] assert args.strategy_path is None assert args.datadir is None assert args.loglevel == 0 @@ -24,10 +24,15 @@ def test_parse_args_defaults() -> None: def test_parse_args_config() -> None: args = Arguments(['-c', '/dev/null'], '').get_parsed_arg() - assert args.config == '/dev/null' + assert args.config == ['/dev/null'] args = Arguments(['--config', '/dev/null'], '').get_parsed_arg() - assert args.config == '/dev/null' + assert args.config == ['/dev/null'] + + args = Arguments(['--config', '/dev/null', + '--config', '/dev/zero'], + '').get_parsed_arg() + assert args.config == ['/dev/null', '/dev/zero'] def test_parse_args_db_url() -> None: @@ -139,7 +144,7 @@ def test_parse_args_backtesting_custom() -> None: 'TestStrategy' ] call_args = Arguments(args, '').get_parsed_arg() - assert call_args.config == 'test_conf.json' + assert call_args.config == ['test_conf.json'] assert call_args.live is True assert call_args.loglevel == 0 assert call_args.subparser == 'backtesting' @@ -158,7 +163,7 @@ def test_parse_args_hyperopt_custom() -> None: '--spaces', 'buy' ] call_args = Arguments(args, '').get_parsed_arg() - assert call_args.config == 'test_conf.json' + assert call_args.config == ['test_conf.json'] assert call_args.epochs == 20 assert call_args.loglevel == 0 assert call_args.subparser == 'hyperopt' diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 67445238b..9f11e8ac2 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -50,18 +50,20 @@ def test_load_config_file(default_conf, mocker, caplog) -> None: validated_conf = configuration._load_config_file('somefile') assert file_mock.call_count == 1 assert validated_conf.items() >= default_conf.items() - assert 'internals' in validated_conf - assert log_has('Validating configuration ...', caplog.record_tuples) def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = 0 - file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( + mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) - Configuration(Namespace())._load_config_file('somefile') - assert file_mock.call_count == 1 + args = Arguments([], '').get_parsed_arg() + configuration = Configuration(args) + validated_conf = configuration.load_config() + + assert validated_conf['max_open_trades'] == 0 + assert 'internals' in validated_conf assert log_has('Validating configuration ...', caplog.record_tuples) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 7aae98ebe..51c95a4a9 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -22,7 +22,7 @@ def test_parse_args_backtesting(mocker) -> None: main(['backtesting']) assert backtesting_mock.call_count == 1 call_args = backtesting_mock.call_args[0][0] - assert call_args.config == 'config.json' + assert call_args.config == ['config.json'] assert call_args.live is False assert call_args.loglevel == 0 assert call_args.subparser == 'backtesting' @@ -35,7 +35,7 @@ def test_main_start_hyperopt(mocker) -> None: main(['hyperopt']) assert hyperopt_mock.call_count == 1 call_args = hyperopt_mock.call_args[0][0] - assert call_args.config == 'config.json' + assert call_args.config == ['config.json'] assert call_args.loglevel == 0 assert call_args.subparser == 'hyperopt' assert call_args.func is not None From da5bef501e4a3f7330beac5c1d44b3192385515b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 20 Feb 2019 17:55:20 +0300 Subject: [PATCH 10/46] cleanup --- scripts/download_backtest_data.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index df3e4bb0c..5dee41bdd 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -39,8 +39,6 @@ if args.config: # Merge config options, overwriting old values config = deep_merge_dicts(configuration._load_config_file(path), config) -### config = configuration._load_config_file(args.config) - config['stake_currency'] = '' # Ensure we do not use Exchange credentials config['exchange']['key'] = '' From b5758e67f90680afbbe696f64d71add57952ab45 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Thu, 21 Feb 2019 00:29:59 +0100 Subject: [PATCH 11/46] order creation cleanup --- freqtrade/exchange/__init__.py | 129 ++++++++++++--------------------- 1 file changed, 46 insertions(+), 83 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 078c8e6be..170f94182 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -284,9 +284,9 @@ class Exchange(object): return price def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Dict: - order_id = f'dry_run_buy_{randint(0, 10**6)}' - dry_order = { + rate: float, params: Dict = {}) -> Tuple[str, Dict[str, Any]]: + order_id = f'dry_run_{side}_{randint(0, 10**6)}' + dry_order = { # TODO: ad additional entry should be added for stoploss limit "id": order_id, 'pair': pair, 'price': rate, @@ -302,84 +302,68 @@ class Exchange(object): def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict: - pass # TODO: finish this + try: + return self._api.create_order(pair, ordertype, side, + amount, rate, params) + + except ccxt.InsufficientFunds as e: + raise DependencyException( + f'Insufficient funds to create {ordertype} {side} order on market {pair}.' + f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).' + f'Message: {e}') + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not create {ordertype} {side} order on market {pair}.' + f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).' + f'Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) def buy(self, pair: str, ordertype: str, amount: float, rate: float, time_in_force) -> Dict: + if self._conf['dry_run']: order_id, dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate) self._dry_run_open_orders[order_id] = dry_order return {'id': order_id} - try: - # Set the precision for amount and price(rate) as accepted by the exchange - amount = self.symbol_amount_prec(pair, amount) - rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None + # Set the precision for amount and price(rate) as accepted by the exchange + amount = self.symbol_amount_prec(pair, amount) + rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None - params = self._params.copy() - if time_in_force != 'gtc': - params.update({'timeInForce': time_in_force}) + params = self._params.copy() + if time_in_force != 'gtc': + params.update({'timeInForce': time_in_force}) - return self._api.create_order(pair, ordertype, 'buy', - amount, rate, params) - - except ccxt.InsufficientFunds as e: - raise DependencyException( - f'Insufficient funds to create limit buy order on market {pair}.' - f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).' - f'Message: {e}') - except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not create limit buy order on market {pair}.' - f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).' - f'Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not place buy order due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) + return self.create_order(pair, ordertype, 'buy', amount, rate, params) def sell(self, pair: str, ordertype: str, amount: float, rate: float, time_in_force='gtc') -> Dict: + if self._conf['dry_run']: order_id, dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate) self._dry_run_open_orders[order_id] = dry_order return {'id': order_id} - try: - # Set the precision for amount and price(rate) as accepted by the exchange - amount = self.symbol_amount_prec(pair, amount) - rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None + # Set the precision for amount and price(rate) as accepted by the exchange + amount = self.symbol_amount_prec(pair, amount) + rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None - params = self._params.copy() - if time_in_force != 'gtc': - params.update({'timeInForce': time_in_force}) + params = self._params.copy() + if time_in_force != 'gtc': + params.update({'timeInForce': time_in_force}) - return self._api.create_order(pair, ordertype, 'sell', - amount, rate, params) - - except ccxt.InsufficientFunds as e: - raise DependencyException( - f'Insufficient funds to create limit sell order on market {pair}.' - f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).' - f'Message: {e}') - except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not create limit sell order on market {pair}.' - f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).' - f'Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) + return self.create_order(pair, ordertype, 'sell', amount, rate, params) def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: """ creates a stoploss limit order. NOTICE: it is not supported by all exchanges. only binance is tested for now. """ - + ordertype = "stop_loss_limit" # Set the precision for amount and price(rate) as accepted by the exchange amount = self.symbol_amount_prec(pair, amount) rate = self.symbol_price_prec(pair, rate) @@ -392,39 +376,18 @@ class Exchange(object): if self._conf['dry_run']: order_id, dry_order = self.dry_run_order( - pair, "stop_loss_limit", "sell", amount, stop_price, rate) + pair, ordertype, "sell", amount, stop_price) dry_order.update({"info": {}, "remaining": amount, "status": "open"}) self._dry_run_open_orders[order_id] = dry_order return dry_order - try: + params = self._params.copy() + params.update({'stopPrice': stop_price}) - params = self._params.copy() - params.update({'stopPrice': stop_price}) - - order = self._api.create_order(pair, 'stop_loss_limit', 'sell', - amount, rate, params) - logger.info('stoploss limit order added for %s. ' - 'stop price: %s. limit: %s' % (pair, stop_price, rate)) - return order - - except ccxt.InsufficientFunds as e: - raise DependencyException( - f'Insufficient funds to place stoploss limit order on market {pair}. ' - f'Tried to put a stoploss amount {amount} with ' - f'stop {stop_price} and limit {rate} (total {rate*amount}).' - f'Message: {e}') - except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not place stoploss limit order on market {pair}.' - f'Tried to place stoploss amount {amount} with ' - f'stop {stop_price} and limit {rate} (total {rate*amount}).' - f'Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not place stoploss limit order due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) + order = self.create_order(pair, ordertype, 'sell', amount, rate, params) + logger.info('stoploss limit order added for %s. ' + 'stop price: %s. limit: %s' % (pair, stop_price, rate)) + return order @retrier def get_balance(self, currency: str) -> float: From 69bb6ebaf6506ad7f65a72379880ea87d9539344 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Thu, 21 Feb 2019 22:43:15 +0100 Subject: [PATCH 12/46] fix comments --- freqtrade/exchange/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index db77d0d5c..2580bf6b1 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -285,7 +285,7 @@ class Exchange(object): def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Tuple[str, Dict[str, Any]]: order_id = f'dry_run_{side}_{randint(0, 10**6)}' - dry_order = { # TODO: ad additional entry should be added for stoploss limit + dry_order = { # TODO: additional entry should be added for stoploss limit "id": order_id, 'pair': pair, 'price': rate, @@ -295,7 +295,7 @@ class Exchange(object): 'remaining': 0.0, 'datetime': arrow.utcnow().isoformat(), 'status': 'closed', - 'fee': None # should this be None or skipped? + 'fee': None } return order_id, dry_order From b79d967371c175f38706bb6409181d71d85f09de Mon Sep 17 00:00:00 2001 From: iuvbio Date: Fri, 22 Feb 2019 01:48:35 +0100 Subject: [PATCH 13/46] add tests, further consolidate orders --- freqtrade/exchange/__init__.py | 36 +++++++--------- freqtrade/tests/exchange/test_exchange.py | 50 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 2580bf6b1..26fd56b59 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -283,7 +283,7 @@ class Exchange(object): return price def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Tuple[str, Dict[str, Any]]: + rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{randint(0, 10**6)}' dry_order = { # TODO: additional entry should be added for stoploss limit "id": order_id, @@ -297,11 +297,15 @@ class Exchange(object): 'status': 'closed', 'fee': None } - return order_id, dry_order + return dry_order def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict: try: + # Set the precision for amount and price(rate) as accepted by the exchange + amount = self.symbol_amount_prec(pair, amount) + rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None + return self._api.create_order(pair, ordertype, side, amount, rate, params) @@ -325,13 +329,9 @@ class Exchange(object): rate: float, time_in_force) -> Dict: if self._conf['dry_run']: - order_id, dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate) - self._dry_run_open_orders[order_id] = dry_order - return {'id': order_id} - - # Set the precision for amount and price(rate) as accepted by the exchange - amount = self.symbol_amount_prec(pair, amount) - rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None + dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate) + self._dry_run_open_orders[dry_order["id"]] = dry_order + return {"id": dry_order["id"]} params = self._params.copy() if time_in_force != 'gtc': @@ -343,13 +343,9 @@ class Exchange(object): rate: float, time_in_force='gtc') -> Dict: if self._conf['dry_run']: - order_id, dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate) - self._dry_run_open_orders[order_id] = dry_order - return {'id': order_id} - - # Set the precision for amount and price(rate) as accepted by the exchange - amount = self.symbol_amount_prec(pair, amount) - rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None + dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate) + self._dry_run_open_orders[dry_order["id"]] = dry_order + return {"id": dry_order["id"]} params = self._params.copy() if time_in_force != 'gtc': @@ -363,9 +359,7 @@ class Exchange(object): NOTICE: it is not supported by all exchanges. only binance is tested for now. """ ordertype = "stop_loss_limit" - # Set the precision for amount and price(rate) as accepted by the exchange - amount = self.symbol_amount_prec(pair, amount) - rate = self.symbol_price_prec(pair, rate) + stop_price = self.symbol_price_prec(pair, stop_price) # Ensure rate is less than stop price @@ -374,10 +368,10 @@ class Exchange(object): 'In stoploss limit order, stop price should be more than limit price') if self._conf['dry_run']: - order_id, dry_order = self.dry_run_order( + dry_order = self.dry_run_order( pair, ordertype, "sell", amount, stop_price) dry_order.update({"info": {}, "remaining": amount, "status": "open"}) - self._dry_run_open_orders[order_id] = dry_order + self._dry_run_open_orders[dry_order["id"]] = dry_order return dry_order params = self._params.copy() diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 72919103c..c646c0db7 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -441,6 +441,56 @@ def test_exchange_has(default_conf, mocker): assert not exchange.exchange_has("deadbeef") +@pytest.mark.parametrize("side", [ + ("buy"), + ("sell") +]) +def test_dry_run_order(default_conf, mocker, side): + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf) + + order = exchange.dry_run_order( + pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) + assert 'id' in order + assert f'dry_run_{side}_' in order["id"] + + +@pytest.mark.parametrize("side", [ + ("buy"), + ("sell") +]) +@pytest.mark.parametrize("ordertype,rate", [ + ("market", None), + ("limit", 200), + ("stop_loss_limit", 200) +]) +def test_create_order(default_conf, mocker, side, ordertype, rate): + api_mock = MagicMock() + order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + order = exchange.create_order( + pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == ordertype + assert api_mock.create_order.call_args[0][2] == side + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] is rate + + def test_buy_dry_run(default_conf, mocker): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf) From 9a097214a615f40aa10473b13984cf5d61a7c9d2 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Fri, 22 Feb 2019 19:22:48 +0100 Subject: [PATCH 14/46] return complete dry_order in buy and sell --- freqtrade/exchange/exchange.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 26fd56b59..c3d562aa9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -331,7 +331,7 @@ class Exchange(object): if self._conf['dry_run']: dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate) self._dry_run_open_orders[dry_order["id"]] = dry_order - return {"id": dry_order["id"]} + return dry_order # {"id": dry_order["id"]} params = self._params.copy() if time_in_force != 'gtc': @@ -345,7 +345,7 @@ class Exchange(object): if self._conf['dry_run']: dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate) self._dry_run_open_orders[dry_order["id"]] = dry_order - return {"id": dry_order["id"]} + return dry_order # {"id": dry_order["id"]} params = self._params.copy() if time_in_force != 'gtc': @@ -527,7 +527,7 @@ class Exchange(object): interval_in_sec = constants.TICKER_INTERVAL_MINUTES[ticker_interval] * 60 return not ((self._pairs_last_refresh_time.get((pair, ticker_interval), 0) - + interval_in_sec) >= arrow.utcnow().timestamp) + + interval_in_sec) >= arrow.utcnow().timestamp) @retrier_async async def _async_get_candle_history(self, pair: str, tick_interval: str, From 634ce87bba37caedbbab1e6956b483a276c8da45 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 23 Feb 2019 13:32:04 +0100 Subject: [PATCH 15/46] Update ccxt from 1.18.281 to 1.18.287 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cd1d89c6d..aa6051bdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.281 +ccxt==1.18.287 SQLAlchemy==1.2.18 python-telegram-bot==11.1.0 arrow==0.13.1 From ec6794b9ba6810ce176e6adc1c9af68114501db4 Mon Sep 17 00:00:00 2001 From: iuvbio Date: Sat, 23 Feb 2019 16:03:15 +0100 Subject: [PATCH 16/46] fix dry_orders --- freqtrade/exchange/exchange.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c3d562aa9..94c0e6b3b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -290,15 +290,28 @@ class Exchange(object): 'pair': pair, 'price': rate, 'amount': amount, + "cost": amount * rate, 'type': ordertype, 'side': 'buy', - 'remaining': 0.0, + 'remaining': amount, 'datetime': arrow.utcnow().isoformat(), - 'status': 'closed', - 'fee': None + 'status': "open", + 'fee': None, + "info": {} } + self.store_dry_order(dry_order) return dry_order + def store_dry_order(self, dry_order: Dict) -> None: + closed_order = dry_order.copy() + if closed_order["type"] in ["market", "limit"]: + closed_order.update({ + "status": "closed", + "filled": closed_order["amount"], + "remaining": 0 + }) + self._dry_run_open_orders[closed_order["id"]] = closed_order + def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict: try: @@ -330,8 +343,7 @@ class Exchange(object): if self._conf['dry_run']: dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate) - self._dry_run_open_orders[dry_order["id"]] = dry_order - return dry_order # {"id": dry_order["id"]} + return dry_order params = self._params.copy() if time_in_force != 'gtc': @@ -344,8 +356,7 @@ class Exchange(object): if self._conf['dry_run']: dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate) - self._dry_run_open_orders[dry_order["id"]] = dry_order - return dry_order # {"id": dry_order["id"]} + return dry_order params = self._params.copy() if time_in_force != 'gtc': @@ -370,8 +381,6 @@ class Exchange(object): if self._conf['dry_run']: dry_order = self.dry_run_order( pair, ordertype, "sell", amount, stop_price) - dry_order.update({"info": {}, "remaining": amount, "status": "open"}) - self._dry_run_open_orders[dry_order["id"]] = dry_order return dry_order params = self._params.copy() @@ -586,9 +595,6 @@ class Exchange(object): def get_order(self, order_id: str, pair: str) -> Dict: if self._conf['dry_run']: order = self._dry_run_open_orders[order_id] - order.update({ - 'id': order_id - }) return order try: return self._api.fetch_order(order_id, pair) From 403ed48c3e06537005e0efa024b727f8f2a7dccc Mon Sep 17 00:00:00 2001 From: iuvbio Date: Sat, 23 Feb 2019 16:28:13 +0100 Subject: [PATCH 17/46] rename _store_dry_order --- freqtrade/exchange/exchange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 94c0e6b3b..23926d00f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -299,10 +299,10 @@ class Exchange(object): 'fee': None, "info": {} } - self.store_dry_order(dry_order) + self._store_dry_order(dry_order) return dry_order - def store_dry_order(self, dry_order: Dict) -> None: + def _store_dry_order(self, dry_order: Dict) -> None: closed_order = dry_order.copy() if closed_order["type"] in ["market", "limit"]: closed_order.update({ From 5fac4f7b45a99c5d70c81f6a92910fa86178109c Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sun, 24 Feb 2019 13:09:32 +0300 Subject: [PATCH 18/46] Edge doc file minor improvements, typos, formatting --- docs/edge.md | 197 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 118 insertions(+), 79 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index b208cb318..a4acffc44 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -3,159 +3,198 @@ This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss. !!! Warning - Edge positioning is not compatible with dynamic whitelist. it overrides dynamic whitelist. + Edge positioning is not compatible with dynamic whitelist. If enabled, it overrides the dynamic whitelist option. !!! Note - Edge won't consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else will be ignored in its calculation. + Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation. ## Introduction -Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose.

-But it doesn't mean there is no rule, it only means rules should work "most of the time". Let's play a game: we toss a coin, heads: I give you 10$, tails: You give me 10$. Is it an interesting game ? no, it is quite boring, isn't it?

-But let's say the probability that we have heads is 80%, and the probability that we have tails is 20%. Now it is becoming interesting ... -That means 10$ x 80% versus 10$ x 20%. 8$ versus 2$. That means over time you will win 8$ risking only 2$ on each toss of coin.

-Let's complicate it more: you win 80% of the time but only 2$, I win 20% of the time but 8$. The calculation is: 80% * 2$ versus 20% * 8$. It is becoming boring again because overtime you win $1.6$ (80% x 2$) and me $1.6 (20% * 8$) too.

-The question is: How do you calculate that? how do you know if you wanna play? +Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose. + +But it doesn't mean there is no rule, it only means rules should work "most of the time". Let's play a game: we toss a coin, heads: I give you 10$, tails: you give me 10$. Is it an interesting game? No, it's quite boring, isn't it? + +But let's say the probability that we have heads is 80% (because our coin has the displaced distribution of mass or other defect), and the probability that we have tails is 20%. Now it is becoming interesting... + +That means 10$ X 80% versus 10$ X 20%. 8$ versus 2$. That means over time you will win 8$ risking only 2$ on each toss of coin. + +Let's complicate it more: you win 80% of the time but only 2$, I win 20% of the time but 8$. The calculation is: 80% X 2$ versus 20% X 8$. It is becoming boring again because overtime you win $1.6$ (80% X 2$) and me $1.6 (20% X 8$) too. + +The question is: How do you calculate that? How do you know if you wanna play? + The answer comes to two factors: - Win Rate - Risk Reward Ratio - ### Win Rate -Means over X trades what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only If you won or not). +Win Rate (*W*) is is the mean over some amount of trades (*N*) what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only if you won or not). + W = (Number of winning trades) / (Total number of trades) = (Number of winning trades) / N -`W = (Number of winning trades) / (Total number of trades)` +Complementary Loss Rate (*L*) is defined as + + L = (Number of losing trades) / (Total number of trades) = (Number of losing trades) / N + +or, which is the same, as + + R = 1 – W ### Risk Reward Ratio -Risk Reward Ratio is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose: +Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose: -`R = Profit / Loss` + R = Profit / Loss Over time, on many trades, you can calculate your risk reward by dividing your average profit on winning trades by your average loss on losing trades: -`Average profit = (Sum of profits) / (Number of winning trades)` + Average profit = (Sum of profits) / (Number of winning trades) -`Average loss = (Sum of losses) / (Number of losing trades)` + Average loss = (Sum of losses) / (Number of losing trades) -`R = (Average profit) / (Average loss)` + R = (Average profit) / (Average loss) ### Expectancy +At this point we can combine *W* and *R* to create an expectancy ratio. This is a simple process of multiplying the risk reward ratio by the percentage of winning trades and subtracting the percentage of losing trades, which is calculated as follows: -At this point we can combine W and R to create an expectancy ratio. This is a simple process of multiplying the risk reward ratio by the percentage of winning trades, and subtracting the percentage of losing trades, which is calculated as follows: - -Expectancy Ratio = (Risk Reward Ratio x Win Rate) – Loss Rate + Expectancy Ratio = (Risk Reward Ratio X Win Rate) – Loss Rate = (R X W) – L So lets say your Win rate is 28% and your Risk Reward Ratio is 5: -`Expectancy = (5 * 0.28) - 0.72 = 0.68` + Expectancy = (5 X 0.28) – 0.72 = 0.68 -Superficially, this means that on average you expect this strategy’s trades to return .68 times the size of your losers. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. +Superficially, this means that on average you expect this strategy’s trades to return .68 times the size of your loses. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. It is important to remember that any system with an expectancy greater than 0 is profitable using past data. The key is finding one that will be profitable in the future. -You can also use this number to evaluate the effectiveness of modifications to this system. +You can also use this value to evaluate the effectiveness of modifications to this system. -**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data , there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology, but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. +**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology, but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. ## How does it work? -If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over X trades for each stoploss. Here is an example: +If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over *N* trades for each stoploss. Here is an example: | Pair | Stoploss | Win Rate | Risk Reward Ratio | Expectancy | |----------|:-------------:|-------------:|------------------:|-----------:| -| XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 | | XZC/ETH | -0.01 | 0.50 |1.176384 | 0.088 | | XZC/ETH | -0.02 | 0.51 |1.115941 | 0.079 | +| XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 | +| XZC/ETH | -0.04 | 0.51 |1.234539 | 0.117 | The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at 3% leads to the maximum expectancy according to historical data. -Edge then forces stoploss to your strategy dynamically. +Edge module then forces stoploss value it evaluated to your strategy dynamically. ### Position size -Edge dictates the stake amount for each trade to the bot according to the following factors: +Edge also dictates the stake amount for each trade to the bot according to the following factors: - Allowed capital at risk - Stoploss Allowed capital at risk is calculated as follows: -**allowed capital at risk** = **capital_available_percentage** X **allowed risk per trade** + Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade) -**Stoploss** is calculated as described above against historical data. +Stoploss is calculated as described above against historical data. Your position size then will be: -**position size** = **allowed capital at risk** / **stoploss** + Position size = (Allowed capital at risk) / Stoploss -Example:
-Let's say the stake currency is ETH and you have 10 ETH on the exchange, your **capital_available_percentage** is 50% and you would allow 1% of risk for each trade. thus your available capital for trading is **10 x 0.5 = 5 ETH** and allowed capital at risk would be **5 x 0.01 = 0.05 ETH**.
-Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.05 / 0.02 = 2.5ETH**.
-Bot takes a position of 2.5ETH on XLM/ETH (call it trade 1). Up next, you receive another buy signal while trade 1 is still open. This time on BTC/ETH market. Edge calculated stoploss for this market at 4%. So your position size would be 0.05 / 0.04 = 1.25ETH (call it trade 2).
-Note that available capital for trading didn’t change for trade 2 even if you had already trade 1. The available capital doesn’t mean the free amount on your wallet.
-Now you have two trades open. The Bot receives yet another buy signal for another market: **ADA/ETH**. This time the stoploss is calculated at 1%. So your position size is **0.05 / 0.01 = 5ETH**. But there are already 4ETH blocked in two previous trades. So the position size for this third trade would be 1ETH.
-Available capital doesn’t change before a position is sold. Let’s assume that trade 1 receives a sell signal and it is sold with a profit of 1ETH. Your total capital on exchange would be 11 ETH and the available capital for trading becomes 5.5ETH.
-So the Bot receives another buy signal for trade 4 with a stoploss at 2% then your position size would be **0.055 / 0.02 = 2.75**. +Example: + +Let's say the stake currency is ETH and you have 10 ETH on the exchange, your capital available percentage is 50% and you would allow 1% of risk for each trade. thus your available capital for trading is **10 x 0.5 = 5 ETH** and allowed capital at risk would be **5 x 0.01 = 0.05 ETH**. + +Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.05 / 0.02 = 2.5 ETH**. + +Bot takes a position of 2.5 ETH on XLM/ETH (call it trade 1). Up next, you receive another buy signal while trade 1 is still open. This time on **BTC/ETH** market. Edge calculated stoploss for this market at 4%. So your position size would be 0.05 / 0.04 = 1.25 ETH (call it trade 2). + +Note that available capital for trading didn’t change for trade 2 even if you had already trade 1. The available capital doesn’t mean the free amount on your wallet. + +Now you have two trades open. The bot receives yet another buy signal for another market: **ADA/ETH**. This time the stoploss is calculated at 1%. So your position size is **0.05 / 0.01 = 5 ETH**. But there are already 3.75 ETH blocked in two previous trades. So the position size for this third trade would be **5 – 3.75 = 1.25 ETH**. + +Available capital doesn’t change before a position is sold. Let’s assume that trade 1 receives a sell signal and it is sold with a profit of 1 ETH. Your total capital on exchange would be 11 ETH and the available capital for trading becomes 5.5 ETH. + +So the Bot receives another buy signal for trade 4 with a stoploss at 2% then your position size would be **0.055 / 0.02 = 2.75 ETH**. ## Configurations -Edge has following configurations: +Edge module has following configuration options: #### enabled -If true, then Edge will run periodically.
-(default to false) +If true, then Edge will run periodically. + +(defaults to false) #### process_throttle_secs -How often should Edge run in seconds?
-(default to 3600 so one hour) +How often should Edge run in seconds? + +(defaults to 3600 so one hour) #### calculate_since_number_of_days Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy -Note that it downloads historical data so increasing this number would lead to slowing down the bot.
-(default to 7) +Note that it downloads historical data so increasing this number would lead to slowing down the bot. + +(defaults to 7) #### capital_available_percentage -This is the percentage of the total capital on exchange in stake currency.
-As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
-(default to 0.5) +This is the percentage of the total capital on exchange in stake currency. + +As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital. + +(defaults to 0.5) #### allowed_risk -Percentage of allowed risk per trade.
-(default to 0.01 [1%]) +Percentage of allowed risk per trade. + +(defaults to 0.01 so 1%) #### stoploss_range_min -Minimum stoploss.
-(default to -0.01) +Minimum stoploss. + +(defaults to -0.01) #### stoploss_range_max -Maximum stoploss.
-(default to -0.10) +Maximum stoploss. + +(defaults to -0.10) #### stoploss_range_step -As an example if this is set to -0.01 then Edge will test the strategy for [-0.01, -0,02, -0,03 ..., -0.09, -0.10] ranges. -Note than having a smaller step means having a bigger range which could lead to slow calculation.
-if you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10.
-(default to -0.01) +As an example if this is set to -0.01 then Edge will test the strategy for \[-0.01, -0,02, -0,03 ..., -0.09, -0.10\] ranges. +Note than having a smaller step means having a bigger range which could lead to slow calculation. + +If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10. + +(defaults to -0.01) #### minimum_winrate -It filters pairs which don't have at least minimum_winrate. -This comes handy if you want to be conservative and don't comprise win rate in favor of risk reward ratio.
-(default to 0.60) +It filters out pairs which don't have at least minimum_winrate. + +This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. + +(defaults to 0.60) #### minimum_expectancy -It filters paris which have an expectancy lower than this number . -Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return.
-(default to 0.20) +It filters out pairs which have the expectancy lower than this number. + +Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return. + +(defaults to 0.20) #### min_trade_number -When calculating W and R and E (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something.
-(default to 10, it is highly recommended not to decrease this number) +When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. + +Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. + +(defaults to 10, it is highly recommended not to decrease this number) #### max_trade_duration_minute -Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.
-**NOTICE:** While configuring this value, you should take into consideration your ticker interval. as an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. default value is set assuming your strategy interval is relatively small (1m or 5m, etc).
-(default to 1 day, 1440 = 60 * 24) +Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign. + +**NOTICE:** While configuring this value, you should take into consideration your ticker interval. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.). + +(defaults to 1 day, i.e. to 60 * 24 = 1440 minutes) #### remove_pumps -Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.
-(default to false) +Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off. + +(defaults to false) ## Running Edge independently @@ -199,14 +238,14 @@ python3 ./freqtrade/main.py edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step python3 ./freqtrade/main.py edge --timerange=20181110-20181113 ``` -Doing --timerange=-200 will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop. +Doing `--timerange=-200` will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop. The full timerange specification: -* Use last 123 tickframes of data: --timerange=-123 -* Use first 123 tickframes of data: --timerange=123- -* Use tickframes from line 123 through 456: --timerange=123-456 -* Use tickframes till 2018/01/31: --timerange=-20180131 -* Use tickframes since 2018/01/31: --timerange=20180131- -* Use tickframes since 2018/01/31 till 2018/03/01 : --timerange=20180131-20180301 -* Use tickframes between POSIX timestamps 1527595200 1527618600: --timerange=1527595200-1527618600 +* Use last 123 tickframes of data: `--timerange=-123` +* Use first 123 tickframes of data: `--timerange=123-` +* Use tickframes from line 123 through 456: `--timerange=123-456` +* Use tickframes till 2018/01/31: `--timerange=-20180131` +* Use tickframes since 2018/01/31: `--timerange=20180131-` +* Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` +* Use tickframes between POSIX timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600` From 9b288c6933792353c1db7209df0fffd7153b435e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Feb 2019 13:29:22 +0100 Subject: [PATCH 19/46] Add test to specifically test for merged dict --- freqtrade/tests/test_configuration.py | 37 ++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 9f11e8ac2..51098baaa 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -1,15 +1,15 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name import json -from argparse import Namespace import logging +from argparse import Namespace +from copy import deepcopy from unittest.mock import MagicMock import pytest -from jsonschema import validate, ValidationError, Draft4Validator +from jsonschema import Draft4Validator, ValidationError, validate -from freqtrade import constants -from freqtrade import OperationalException +from freqtrade import OperationalException, constants from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration, set_loggers from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL @@ -67,6 +67,35 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: assert log_has('Validating configuration ...', caplog.record_tuples) +def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: + conf1 = deepcopy(default_conf) + conf2 = deepcopy(default_conf) + del conf1['exchange']['key'] + del conf1['exchange']['secret'] + del conf2['exchange']['name'] + conf2['exchange']['pair_whitelist'] += ['NANO/BTC'] + + config_files = [conf1, conf2] + + configsmock = MagicMock(side_effect=config_files) + mocker.patch('freqtrade.configuration.Configuration._load_config_file', configsmock) + + arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ] + args = Arguments(arg_list, '').get_parsed_arg() + configuration = Configuration(args) + validated_conf = configuration.load_config() + + exchange_conf = default_conf['exchange'] + assert validated_conf['exchange']['name'] == exchange_conf['name'] + assert validated_conf['exchange']['key'] == exchange_conf['key'] + assert validated_conf['exchange']['secret'] == exchange_conf['secret'] + assert validated_conf['exchange']['pair_whitelist'] != conf1['exchange']['pair_whitelist'] + assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist'] + + assert 'internals' in validated_conf + assert log_has('Validating configuration ...', caplog.record_tuples) + + def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None: default_conf['max_open_trades'] = -1 mocker.patch('freqtrade.configuration.open', mocker.mock_open( From 3673dba1e2020e0dc27a3703dcd5087daadf7005 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 24 Feb 2019 13:32:05 +0100 Subject: [PATCH 20/46] Update ccxt from 1.18.287 to 1.18.290 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index aa6051bdd..13584ebc0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.287 +ccxt==1.18.290 SQLAlchemy==1.2.18 python-telegram-bot==11.1.0 arrow==0.13.1 From 417bf2c93538fe8cd48ee03f1f1b2786b112327e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 24 Feb 2019 13:32:06 +0100 Subject: [PATCH 21/46] Update jsonschema from 2.6.0 to 3.0.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 13584ebc0..3257de6e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pandas==0.24.1 scikit-learn==0.20.2 joblib==0.13.2 scipy==1.2.1 -jsonschema==2.6.0 +jsonschema==3.0.0 TA-Lib==0.4.17 tabulate==0.8.3 coinmarketcap==5.0.3 From 06f486a8ebd840c4ea9c0567f187a644be89701e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Feb 2019 19:30:05 +0100 Subject: [PATCH 22/46] Add binance exchange subclass --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/binance.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 freqtrade/exchange/binance.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 204ed971e..f6db04da6 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,2 +1,3 @@ from freqtrade.exchange.exchange import Exchange # noqa: F401 from freqtrade.exchange.kraken import Kraken # noqa: F401 +from freqtrade.exchange.binance import Binance # noqa: F401 diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py new file mode 100644 index 000000000..c8bb92168 --- /dev/null +++ b/freqtrade/exchange/binance.py @@ -0,0 +1,26 @@ +""" Binance exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + +logger = logging.getLogger(__name__) + + +class Binance(Exchange): + + _ft_has = { + "stoploss_on_exchange": True, + } + + def get_order_book(self, pair: str, limit: int = 100) -> dict: + """ + get order book level 2 from exchange + + 20180619: binance support limits but only on specific range + """ + limit_range = [5, 10, 20, 50, 100, 500, 1000] + # get next-higher step in the limit_range list + limit = min(list(filter(lambda x: limit <= x, limit_range))) + + return self.super().get_order_book(pair, limit) From 455b16836680fa1c16da53edcff9fe9ca38bfc5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Feb 2019 19:35:29 +0100 Subject: [PATCH 23/46] add _ft_has to exchangeclass --- freqtrade/exchange/exchange.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 23926d00f..b6ff261af 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -24,7 +24,7 @@ API_RETRY_COUNT = 4 # Urls to exchange markets, insert quote and base with .format() _EXCHANGE_URLS = { ccxt.bittrex.__name__: '/Market/Index?MarketName={quote}-{base}', - ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}' + ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}', } @@ -69,11 +69,17 @@ class Exchange(object): _conf: Dict = {} _params: Dict = {} + # Dict to specify which options each exchange implements + # TODO: this should be merged with attributes from subclasses + # To avoid having to copy/paste this to all subclasses. + _ft_has = { + "stoploss_on_exchange": False, + } + def __init__(self, config: dict) -> None: """ Initializes this module with the given config, - it does basic validation whether the specified - exchange and pairs are valid. + it does basic validation whether the specified exchange and pairs are valid. :return: None """ self._conf.update(config) @@ -236,8 +242,8 @@ class Exchange(object): raise OperationalException( f'Exchange {self.name} does not support market orders.') - if order_types.get('stoploss_on_exchange'): - if self.name != 'Binance': + if (order_types.get("stoploss_on_exchange") + and not self._ft_has.get("stoploss_on_exchange", False)): raise OperationalException( 'On exchange stoploss is not supported for %s.' % self.name ) @@ -368,6 +374,7 @@ class Exchange(object): """ creates a stoploss limit order. NOTICE: it is not supported by all exchanges. only binance is tested for now. + TODO: implementation maybe needs to be move to the binance subclass """ ordertype = "stop_loss_limit" @@ -617,15 +624,6 @@ class Exchange(object): 20180619: binance support limits but only on specific range """ try: - if self._api.name == 'Binance': - limit_range = [5, 10, 20, 50, 100, 500, 1000] - # get next-higher step in the limit_range list - limit = min(list(filter(lambda x: limit <= x, limit_range))) - # above script works like loop below (but with slightly better performance): - # for limitx in limit_range: - # if limit <= limitx: - # limit = limitx - # break return self._api.fetch_l2_order_book(pair, limit) except ccxt.NotSupported as e: From a05155cb75efcfb7a2283ee0296db36e4674f318 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Feb 2019 19:41:47 +0100 Subject: [PATCH 24/46] Adapt failing test --- freqtrade/exchange/binance.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index c8bb92168..998f389f0 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -23,4 +23,4 @@ class Binance(Exchange): # get next-higher step in the limit_range list limit = min(list(filter(lambda x: limit <= x, limit_range))) - return self.super().get_order_book(pair, limit) + return super(Binance, self).get_order_book(pair, limit) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 153be89c1..837c767f6 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -113,7 +113,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - exchange = ExchangeResolver('Binance', default_conf).exchange + exchange = ExchangeResolver('Bittrex', default_conf).exchange assert isinstance(exchange, Exchange) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog.record_tuples) From e0b634ba3bd1d7852849b8ee40bf2ac866c38ef0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Feb 2019 19:59:45 +0100 Subject: [PATCH 25/46] Parametrize exchanges and test multiple exchanges --- freqtrade/tests/exchange/test_exchange.py | 127 ++++++++++++---------- 1 file changed, 70 insertions(+), 57 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 837c767f6..e306d7d23 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -18,6 +18,10 @@ from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re from freqtrade.resolvers.exchange_resolver import ExchangeResolver +# Make sure to always keep one exchange here which is NOT subclassed!! +EXCHANGES = ['bittrex', 'binance', 'kraken', ] + + # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines def get_mock_coro(return_value): async def mock_coro(*args, **kwargs): @@ -26,16 +30,17 @@ def get_mock_coro(return_value): return Mock(wraps=mock_coro) -def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): +def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + fun, mock_ccxt_fun, **kwargs): with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 @@ -763,7 +768,8 @@ def test_get_balances_dry_run(default_conf, mocker): assert exchange.get_balances() == {} -def test_get_balances_prod(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_balances_prod(default_conf, mocker, exchange_name): balance_item = { 'free': 10.0, 'total': 10.0, @@ -777,17 +783,18 @@ def test_get_balances_prod(default_conf, mocker): '3ST': balance_item }) default_conf['dry_run'] = False - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert len(exchange.get_balances()) == 3 assert exchange.get_balances()['1ST']['free'] == 10.0 assert exchange.get_balances()['1ST']['total'] == 10.0 assert exchange.get_balances()['1ST']['used'] == 0.0 - ccxt_exceptionhandlers(mocker, default_conf, api_mock, + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_balances", "fetch_balance") -def test_get_tickers(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_tickers(default_conf, mocker, exchange_name): api_mock = MagicMock() tick = {'ETH/BTC': { 'symbol': 'ETH/BTC', @@ -802,7 +809,7 @@ def test_get_tickers(default_conf, mocker): } } api_mock.fetch_tickers = MagicMock(return_value=tick) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker tickers = exchange.get_tickers() @@ -813,20 +820,21 @@ def test_get_tickers(default_conf, mocker): assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['ask'] == 0.5 - ccxt_exceptionhandlers(mocker, default_conf, api_mock, + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") with pytest.raises(OperationalException): api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() api_mock.fetch_tickers = MagicMock(return_value={}) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() -def test_get_ticker(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_ticker(default_conf, mocker, exchange_name): api_mock = MagicMock() tick = { 'symbol': 'ETH/BTC', @@ -836,7 +844,7 @@ def test_get_ticker(default_conf, mocker): } api_mock.fetch_ticker = MagicMock(return_value=tick) api_mock.markets = {'ETH/BTC': {}} - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker ticker = exchange.get_ticker(pair='ETH/BTC') @@ -851,7 +859,7 @@ def test_get_ticker(default_conf, mocker): 'last': 42, } api_mock.fetch_ticker = MagicMock(return_value=tick) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # if not caching the result we should get the same ticker # if not fetching a new result we should get the cached ticker @@ -870,20 +878,21 @@ def test_get_ticker(default_conf, mocker): exchange.get_ticker(pair='ETH/BTC', refresh=False) assert api_mock.fetch_ticker.call_count == 0 - ccxt_exceptionhandlers(mocker, default_conf, api_mock, + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_ticker", "fetch_ticker", pair='ETH/BTC', refresh=True) api_mock.fetch_ticker = MagicMock(return_value={}) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) 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) +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_history(default_conf, mocker, caplog, exchange_name): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) tick = [ [ arrow.utcnow().timestamp * 1000, # unix timestamp ms @@ -1051,12 +1060,13 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): assert log_has("Async code raised an exception: TypeError", caplog.record_tuples) -def test_get_order_book(default_conf, mocker, order_book_l2): - default_conf['exchange']['name'] = 'binance' +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name): + default_conf['exchange']['name'] = exchange_name api_mock = MagicMock() api_mock.fetch_l2_order_book = order_book_l2 - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order_book = exchange.get_order_book(pair='ETH/BTC', limit=10) assert 'bids' in order_book assert 'asks' in order_book @@ -1064,19 +1074,20 @@ def test_get_order_book(default_conf, mocker, order_book_l2): assert len(order_book['asks']) == 10 -def test_get_order_book_exception(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_order_book_exception(default_conf, mocker, exchange_name): api_mock = MagicMock() with pytest.raises(OperationalException): api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) with pytest.raises(TemporaryError): api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) with pytest.raises(OperationalException): api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order_book(pair='ETH/BTC', limit=50) @@ -1089,8 +1100,9 @@ def make_fetch_ohlcv_mock(data): return fetch_ohlcv_mock +@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.asyncio -async def test___async_get_candle_history_sort(default_conf, mocker): +async def test___async_get_candle_history_sort(default_conf, mocker, exchange_name): def sort_data(data, key): return sorted(data, key=key) @@ -1108,7 +1120,7 @@ async def test___async_get_candle_history_sort(default_conf, mocker): [1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687], [1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867] ] - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._api_async.fetch_ohlcv = get_mock_coro(tick) sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data)) # Test the ticker history sort @@ -1170,36 +1182,39 @@ async def test___async_get_candle_history_sort(default_conf, mocker): assert ticks[9][5] == 2.31452783 -def test_cancel_order_dry_run(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_cancel_order_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) assert exchange.cancel_order(order_id='123', pair='TKN/BTC') is None # Ensure that if not dry_run, we should call API -def test_cancel_order(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_cancel_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = False api_mock = MagicMock() api_mock.cancel_order = MagicMock(return_value=123) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 with pytest.raises(DependencyException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.cancel_order(order_id='_', pair='TKN/BTC') assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1 - ccxt_exceptionhandlers(mocker, default_conf, api_mock, + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "cancel_order", "cancel_order", order_id='_', pair='TKN/BTC') -def test_get_order(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order print(exchange.get_order('X', 'TKN/BTC')) assert exchange.get_order('X', 'TKN/BTC').myid == 123 @@ -1207,33 +1222,28 @@ def test_get_order(default_conf, mocker): default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.get_order('X', 'TKN/BTC') == 456 with pytest.raises(DependencyException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 - ccxt_exceptionhandlers(mocker, default_conf, api_mock, + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_order', 'fetch_order', order_id='_', pair='TKN/BTC') -def test_name(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_name(default_conf, mocker, exchange_name): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - default_conf['exchange']['name'] = 'binance' + default_conf['exchange']['name'] = exchange_name exchange = Exchange(default_conf) - assert exchange.name == 'Binance' - - -def test_id(default_conf, mocker): - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - default_conf['exchange']['name'] = 'binance' - exchange = Exchange(default_conf) - assert exchange.id == 'binance' + assert exchange.name == exchange_name.title() + assert exchange.id == exchange_name def test_get_pair_detail_url(default_conf, mocker, caplog): @@ -1267,7 +1277,8 @@ def test_get_pair_detail_url(default_conf, mocker, caplog): assert log_has('Could not get exchange url for Poloniex', caplog.record_tuples) -def test_get_trades_for_order(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_trades_for_order(default_conf, mocker, exchange_name): order_id = 'ABCD-ABCD' since = datetime(2018, 5, 5) default_conf["dry_run"] = False @@ -1294,13 +1305,13 @@ def test_get_trades_for_order(default_conf, mocker): 'amount': 0.2340606, 'fee': {'cost': 0.06179, 'currency': 'BTC'} }]) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since) assert len(orders) == 1 assert orders[0]['price'] == 165 - ccxt_exceptionhandlers(mocker, default_conf, api_mock, + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_trades_for_order', 'fetch_my_trades', order_id=order_id, pair='LTC/BTC', since=since) @@ -1308,10 +1319,11 @@ def test_get_trades_for_order(default_conf, mocker): assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == [] -def test_get_markets(default_conf, mocker, markets): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_markets(default_conf, mocker, markets, exchange_name): api_mock = MagicMock() api_mock.fetch_markets = markets - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) ret = exchange.get_markets() assert isinstance(ret, list) assert len(ret) == 6 @@ -1319,11 +1331,12 @@ def test_get_markets(default_conf, mocker, markets): assert ret[0]["id"] == "ethbtc" assert ret[0]["symbol"] == "ETH/BTC" - ccxt_exceptionhandlers(mocker, default_conf, api_mock, + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_markets', 'fetch_markets') -def test_get_fee(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_fee(default_conf, mocker, exchange_name): api_mock = MagicMock() api_mock.calculate_fee = MagicMock(return_value={ 'type': 'taker', @@ -1331,11 +1344,11 @@ def test_get_fee(default_conf, mocker): 'rate': 0.025, 'cost': 0.05 }) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.get_fee() == 0.025 - ccxt_exceptionhandlers(mocker, default_conf, api_mock, + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'get_fee', 'calculate_fee') From 5c18346cd53f7e5be834a3b7bbbf0168d5d509a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Feb 2019 20:01:20 +0100 Subject: [PATCH 26/46] Add typehint to binance dict --- freqtrade/exchange/binance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 998f389f0..60d35f343 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) class Binance(Exchange): - _ft_has = { + _ft_has: Dict = { "stoploss_on_exchange": True, } From 31be4d24549c6e924573a9f5fa9be82b7dff32e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Feb 2019 20:08:27 +0100 Subject: [PATCH 27/46] Add parametrized tests --- freqtrade/tests/exchange/test_exchange.py | 64 ++++++++++++++--------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index e306d7d23..d904eb2c5 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -12,7 +12,7 @@ import pytest from pandas import DataFrame from freqtrade import DependencyException, OperationalException, TemporaryError -from freqtrade.exchange import Exchange, Kraken +from freqtrade.exchange import Exchange, Kraken, Binance from freqtrade.exchange.exchange import API_RETRY_COUNT from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re from freqtrade.resolvers.exchange_resolver import ExchangeResolver @@ -127,6 +127,15 @@ def test_exchange_resolver(default_conf, mocker, caplog): exchange = ExchangeResolver('Kraken', default_conf).exchange assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) + assert not isinstance(exchange, Binance) + assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", + caplog.record_tuples) + + exchange = ExchangeResolver('Binance', default_conf).exchange + assert isinstance(exchange, Exchange) + assert isinstance(exchange, Binance) + assert not isinstance(exchange, Kraken) + assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog.record_tuples) @@ -452,9 +461,10 @@ def test_exchange_has(default_conf, mocker): ("buy"), ("sell") ]) -def test_dry_run_order(default_conf, mocker, side): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_dry_run_order(default_conf, mocker, side, exchange_name): default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) order = exchange.dry_run_order( pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) @@ -471,7 +481,8 @@ def test_dry_run_order(default_conf, mocker, side): ("limit", 200), ("stop_loss_limit", 200) ]) -def test_create_order(default_conf, mocker, side, ordertype, rate): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_create_order(default_conf, mocker, side, ordertype, rate, exchange_name): api_mock = MagicMock() order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) api_mock.create_order = MagicMock(return_value={ @@ -483,7 +494,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate): default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order( pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) @@ -508,7 +519,8 @@ def test_buy_dry_run(default_conf, mocker): assert 'dry_run_buy_' in order['id'] -def test_buy_prod(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_buy_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'market' @@ -522,7 +534,7 @@ def test_buy_prod(default_conf, mocker): default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) @@ -553,25 +565,25 @@ def test_buy_prod(default_conf, mocker): # test exception handling with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(OperationalException): api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.buy(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200, time_in_force=time_in_force) @@ -676,7 +688,8 @@ def test_sell_dry_run(default_conf, mocker): assert 'dry_run_sell_' in order['id'] -def test_sell_prod(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_sell_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_type = 'market' @@ -690,7 +703,7 @@ def test_sell_prod(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) @@ -715,22 +728,22 @@ def test_sell_prod(default_conf, mocker): # test exception handling with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) with pytest.raises(OperationalException): api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) @@ -741,23 +754,24 @@ def test_get_balance_dry_run(default_conf, mocker): assert exchange.get_balance(currency='BTC') == 999.9 -def test_get_balance_prod(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_balance_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4}}) default_conf['dry_run'] = False - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.get_balance(currency='BTC') == 123.4 with pytest.raises(OperationalException): api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_balance(currency='BTC') with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'): - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={})) exchange.get_balance(currency='BTC') @@ -971,7 +985,8 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: @pytest.mark.asyncio -async def test__async_get_candle_history(default_conf, mocker, caplog): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): tick = [ [ arrow.utcnow().timestamp * 1000, # unix timestamp ms @@ -984,11 +999,10 @@ async def test__async_get_candle_history(default_conf, mocker, caplog): ] caplog.set_level(logging.DEBUG) - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) # Monkey-patch async function exchange._api_async.fetch_ohlcv = get_mock_coro(tick) - exchange = Exchange(default_conf) pair = 'ETH/BTC' res = await exchange._async_get_candle_history(pair, "5m") assert type(res) is tuple @@ -1007,7 +1021,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog): api_mock = MagicMock() with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", (arrow.utcnow().timestamp - 2000) * 1000) From f2fd5205ef043d4d2d0b1d326e144b4079e4359d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Feb 2019 20:13:38 +0100 Subject: [PATCH 28/46] Fix typo --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b6ff261af..de2670de7 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -374,7 +374,7 @@ class Exchange(object): """ creates a stoploss limit order. NOTICE: it is not supported by all exchanges. only binance is tested for now. - TODO: implementation maybe needs to be move to the binance subclass + TODO: implementation maybe needs to be moved to the binance subclass """ ordertype = "stop_loss_limit" From 006635003ef8576348057aa6ec5b3a8227456edd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Feb 2019 20:18:41 +0100 Subject: [PATCH 29/46] Fix small typos --- freqtrade/exchange/exchange.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index de2670de7..42ef46eb8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -244,9 +244,9 @@ class Exchange(object): if (order_types.get("stoploss_on_exchange") and not self._ft_has.get("stoploss_on_exchange", False)): - raise OperationalException( - 'On exchange stoploss is not supported for %s.' % self.name - ) + raise OperationalException( + 'On exchange stoploss is not supported for %s.' % self.name + ) def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: """ @@ -621,7 +621,6 @@ class Exchange(object): Notes: 20180619: bittrex doesnt support limits -.- - 20180619: binance support limits but only on specific range """ try: From 185bd1e53c5fd858161a94210514636a6192a303 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 Feb 2019 13:32:04 +0100 Subject: [PATCH 30/46] Update ccxt from 1.18.290 to 1.18.292 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3257de6e9..1a1d3436c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.290 +ccxt==1.18.292 SQLAlchemy==1.2.18 python-telegram-bot==11.1.0 arrow==0.13.1 From 0c53bd6dd48c71837bc4ffec6cfe468f788e7b13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Feb 2019 20:00:17 +0100 Subject: [PATCH 31/46] Complete refactor, moving query_trades to persistance as get_open_trades --- freqtrade/freqtradebot.py | 12 ++------ freqtrade/persistence.py | 9 +++++- freqtrade/rpc/rpc.py | 8 ++--- freqtrade/tests/test_persistence.py | 45 +++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 295c204e3..392101e26 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -163,7 +163,7 @@ class FreqtradeBot(object): self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist) # Query trades from persistence layer - trades = self._query_trades() + trades = Trade.get_open_trades() # Extend active-pair whitelist with pairs from open trades # It ensures that tickers are downloaded for open trades @@ -200,12 +200,6 @@ class FreqtradeBot(object): self.state = State.STOPPED return state_changed - def _query_trades(self) -> List[Any]: - """ - Query trades from persistence layer - """ - return Trade.query.filter(Trade.is_open.is_(True)).all() - def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): """ Extend whitelist with pairs from open trades @@ -265,7 +259,7 @@ class FreqtradeBot(object): avaliable_amount = self.wallets.get_free(self.config['stake_currency']) if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: - open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all()) + open_trades = len(Trade.get_open_trades()) if open_trades >= self.config['max_open_trades']: logger.warning('Can\'t open a new trade: max number of trades is reached') return None @@ -323,7 +317,7 @@ class FreqtradeBot(object): 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(): + for trade in Trade.get_open_trades(): if trade.pair in whitelist: whitelist.remove(trade.pair) logger.debug('Ignoring %s in pair whitelist', trade.pair) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index f9b34fc64..f603b139f 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -5,7 +5,7 @@ This module contains the class to persist trades into SQLite import logging from datetime import datetime from decimal import Decimal -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, @@ -371,3 +371,10 @@ class Trade(_DECL_BASE): .filter(Trade.is_open.is_(True))\ .scalar() return total_open_stake_amount or 0 + + @staticmethod + def get_open_trades() -> List[Any]: + """ + Query trades from persistence layer + """ + return Trade.query.filter(Trade.is_open.is_(True)).all() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e83d9d41b..5aa9bae35 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -83,7 +83,7 @@ class RPC(object): a remotely exposed function """ # Fetch open trade - trades = Trade.query.filter(Trade.is_open.is_(True)).all() + trades = Trade.get_open_trades() if not trades: raise RPCException('no active trade') else: @@ -118,7 +118,7 @@ class RPC(object): return results def _rpc_status_table(self) -> DataFrame: - trades = Trade.query.filter(Trade.is_open.is_(True)).all() + trades = Trade.get_open_trades() if not trades: raise RPCException('no active order') else: @@ -366,7 +366,7 @@ class RPC(object): if trade_id == 'all': # Execute sell for all open orders - for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): + for trade in Trade.get_open_trades(): _exec_forcesell(trade) Trade.session.flush() return @@ -442,7 +442,7 @@ class RPC(object): if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') - return Trade.query.filter(Trade.is_open.is_(True)).all() + return Trade.get_open_trades() def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index be6efc2ff..a9519e693 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -629,3 +629,48 @@ def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): assert round(trade.stop_loss, 8) == 1.26 assert trade.max_rate == 1.4 assert trade.initial_stop_loss == 0.95 + + +def test_get_open(default_conf, fee): + init(default_conf) + + # Simulate dry_run entries + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + open_order_id='dry_run_buy_12345' + ) + Trade.session.add(trade) + + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + is_open=False, + open_order_id='dry_run_sell_12345' + ) + Trade.session.add(trade) + + # Simulate prod entry + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + open_order_id='prod_buy_12345' + ) + Trade.session.add(trade) + + assert len(Trade.get_open_trades()) == 2 From ef18ddd866e21f23eba077e063c3fba6b920d109 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 26 Feb 2019 13:32:03 +0100 Subject: [PATCH 32/46] Update ccxt from 1.18.292 to 1.18.296 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1a1d3436c..ff62c1a29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.292 +ccxt==1.18.296 SQLAlchemy==1.2.18 python-telegram-bot==11.1.0 arrow==0.13.1 From bcf5b5fdcb5838f79c7397231854b807227a98a6 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 26 Feb 2019 13:32:04 +0100 Subject: [PATCH 33/46] Update flake8 from 3.7.6 to 3.7.7 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 34d59d802..859d80482 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ # Include all requirements to run the bot. -r requirements.txt -flake8==3.7.6 +flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 pytest==4.3.0 From 6c75b8a36a690d06e989fda4279daf4a3a0c6071 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Feb 2019 20:16:34 +0100 Subject: [PATCH 34/46] Remove pair market url --- freqtrade/exchange/exchange.py | 10 ---------- freqtrade/freqtradebot.py | 4 ---- freqtrade/rpc/rpc.py | 1 - freqtrade/rpc/telegram.py | 6 +++--- 4 files changed, 3 insertions(+), 18 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 42ef46eb8..874ed93aa 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -654,16 +654,6 @@ class Exchange(object): except ccxt.BaseError as e: raise OperationalException(e) - def get_pair_detail_url(self, pair: str) -> str: - try: - url_base = self._api.urls.get('www') - base, quote = pair.split('/') - - return url_base + _EXCHANGE_URLS[self._api.id].format(base=base, quote=quote) - except KeyError: - logger.warning('Could not get exchange url for %s', self.name) - return "" - @retrier def get_markets(self) -> List[dict]: try: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d61553305..c51dd4673 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -376,7 +376,6 @@ class FreqtradeBot(object): :return: None """ pair_s = pair.replace('_', '/') - pair_url = self.exchange.get_pair_detail_url(pair) stake_currency = self.config['stake_currency'] fiat_currency = self.config.get('fiat_display_currency', None) time_in_force = self.strategy.order_time_in_force['buy'] @@ -441,7 +440,6 @@ class FreqtradeBot(object): 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), 'pair': pair_s, - 'market_url': pair_url, 'limit': buy_limit_filled_price, 'stake_amount': stake_amount, 'stake_currency': stake_currency, @@ -849,7 +847,6 @@ class FreqtradeBot(object): profit_trade = trade.calc_profit(rate=limit) current_rate = self.exchange.get_ticker(trade.pair)['bid'] profit_percent = trade.calc_profit_percent(limit) - pair_url = self.exchange.get_pair_detail_url(trade.pair) gain = "profit" if profit_percent > 0 else "loss" msg = { @@ -857,7 +854,6 @@ class FreqtradeBot(object): 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'gain': gain, - 'market_url': pair_url, 'limit': limit, 'amount': trade.amount, 'open_rate': trade.open_rate, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5aa9bae35..af64c3d67 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -103,7 +103,6 @@ class RPC(object): results.append(dict( trade_id=trade.id, pair=trade.pair, - market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair), date=arrow.get(trade.open_date), open_rate=trade.open_rate, close_rate=trade.close_rate, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3ce7c9167..9caa7288f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -125,7 +125,7 @@ class Telegram(RPC): else: msg['stake_amount_fiat'] = 0 - message = ("*{exchange}:* Buying [{pair}]({market_url})\n" + message = ("*{exchange}:* Buying {pair}\n" "with limit `{limit:.8f}\n" "({stake_amount:.6f} {stake_currency}").format(**msg) @@ -137,7 +137,7 @@ class Telegram(RPC): msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) - message = ("*{exchange}:* Selling [{pair}]({market_url})\n" + message = ("*{exchange}:* Selling {pair}\n" "*Limit:* `{limit:.8f}`\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" @@ -193,7 +193,7 @@ class Telegram(RPC): messages = [ "*Trade ID:* `{trade_id}`\n" - "*Current Pair:* [{pair}]({market_url})\n" + "*Current Pair:* {pair}\n" "*Open Since:* `{date}`\n" "*Amount:* `{amount}`\n" "*Open Rate:* `{open_rate:.8f}`\n" From 5c3177cc79ab54486af2d226f91b88c2f6405e9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Feb 2019 20:16:47 +0100 Subject: [PATCH 35/46] Adapt documentation to remove market_url --- docs/webhook-config.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index e5509d6c9..2b5365e32 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -41,7 +41,6 @@ Possible parameters are: * exchange * pair -* market_url * limit * stake_amount * stake_amount_fiat @@ -56,7 +55,6 @@ Possible parameters are: * exchange * pair * gain -* market_url * limit * amount * open_rate From 79aac473b300a51f2536aa45cb136c0dd7f350ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Feb 2019 20:17:27 +0100 Subject: [PATCH 36/46] Remove market_url from tests --- freqtrade/tests/exchange/test_exchange.py | 31 ----------------------- freqtrade/tests/rpc/test_rpc.py | 2 -- freqtrade/tests/rpc/test_rpc_telegram.py | 28 +++++--------------- freqtrade/tests/rpc/test_rpc_webhook.py | 3 --- freqtrade/tests/test_freqtradebot.py | 5 ---- 5 files changed, 7 insertions(+), 62 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index d904eb2c5..e24d15aed 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1260,37 +1260,6 @@ def test_name(default_conf, mocker, exchange_name): assert exchange.id == exchange_name -def test_get_pair_detail_url(default_conf, mocker, caplog): - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - default_conf['exchange']['name'] = 'binance' - exchange = Exchange(default_conf) - - url = exchange.get_pair_detail_url('TKN/ETH') - assert 'TKN' in url - assert 'ETH' in url - - url = exchange.get_pair_detail_url('LOOONG/BTC') - assert 'LOOONG' in url - assert 'BTC' in url - - default_conf['exchange']['name'] = 'bittrex' - exchange = Exchange(default_conf) - - url = exchange.get_pair_detail_url('TKN/ETH') - assert 'TKN' in url - assert 'ETH' in url - - url = exchange.get_pair_detail_url('LOOONG/BTC') - assert 'LOOONG' in url - assert 'BTC' in url - - default_conf['exchange']['name'] = 'poloniex' - exchange = Exchange(default_conf) - url = exchange.get_pair_detail_url('LOOONG/BTC') - assert '' == url - assert log_has('Could not get exchange url for Poloniex', caplog.record_tuples) - - @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_trades_for_order(default_conf, mocker, exchange_name): order_id = 'ABCD-ABCD' diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index bb685cad5..2de2668e8 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -51,7 +51,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: 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, @@ -72,7 +71,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: 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, diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 686a92469..9964973e1 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -5,7 +5,7 @@ import re from datetime import datetime from random import randint -from unittest.mock import MagicMock, ANY +from unittest.mock import MagicMock import arrow import pytest @@ -183,7 +183,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, - get_pair_detail_url=MagicMock(), get_fee=fee, get_markets=markets ) @@ -195,7 +194,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: _rpc_trade_status=MagicMock(return_value=[{ 'trade_id': 1, 'pair': 'ETH/BTC', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', 'date': arrow.utcnow(), 'open_rate': 1.099e-05, 'close_rate': None, @@ -270,7 +268,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert '[ETH/BTC]' in msg_mock.call_args_list[0][0][0] + assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None: @@ -721,7 +719,6 @@ def test_forcesell_handle(default_conf, update, ticker, fee, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'profit', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', 'limit': 1.172e-05, 'amount': 90.99181073703367, 'open_rate': 1.099e-05, @@ -776,7 +773,6 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', 'limit': 1.044e-05, 'amount': 90.99181073703367, 'open_rate': 1.099e-05, @@ -796,7 +792,6 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, @@ -823,7 +818,6 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', - 'market_url': ANY, 'limit': 1.098e-05, 'amount': 90.99181073703367, 'open_rate': 1.099e-05, @@ -1100,7 +1094,6 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', 'limit': 1.099e-05, 'stake_amount': 0.001, 'stake_amount_fiat': 0.0, @@ -1108,7 +1101,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: 'fiat_currency': 'USD' }) assert msg_mock.call_args[0][0] \ - == '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \ + == '*Bittrex:* Buying ETH/BTC\n' \ 'with limit `0.00001099\n' \ '(0.001000 BTC,0.000 USD)`' @@ -1129,7 +1122,6 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'exchange': 'Binance', 'pair': 'KEY/ETH', 'gain': 'loss', - 'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH', 'limit': 3.201e-05, 'amount': 1333.3333333333335, 'open_rate': 7.5e-05, @@ -1141,8 +1133,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'sell_reason': SellType.STOP_LOSS.value }) assert msg_mock.call_args[0][0] \ - == ('*Binance:* Selling [KEY/ETH]' - '(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' + == ('*Binance:* Selling KEY/ETH\n' '*Limit:* `0.00003201`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' @@ -1156,7 +1147,6 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'exchange': 'Binance', 'pair': 'KEY/ETH', 'gain': 'loss', - 'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH', 'limit': 3.201e-05, 'amount': 1333.3333333333335, 'open_rate': 7.5e-05, @@ -1167,8 +1157,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'sell_reason': SellType.STOP_LOSS.value }) assert msg_mock.call_args[0][0] \ - == ('*Binance:* Selling [KEY/ETH]' - '(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' + == ('*Binance:* Selling KEY/ETH\n' '*Limit:* `0.00003201`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' @@ -1256,7 +1245,6 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', 'limit': 1.099e-05, 'stake_amount': 0.001, 'stake_amount_fiat': 0.0, @@ -1264,7 +1252,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'fiat_currency': None }) assert msg_mock.call_args[0][0] \ - == '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \ + == '*Bittrex:* Buying ETH/BTC\n' \ 'with limit `0.00001099\n' \ '(0.001000 BTC)`' @@ -1284,7 +1272,6 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'exchange': 'Binance', 'pair': 'KEY/ETH', 'gain': 'loss', - 'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH', 'limit': 3.201e-05, 'amount': 1333.3333333333335, 'open_rate': 7.5e-05, @@ -1296,8 +1283,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'sell_reason': SellType.STOP_LOSS.value }) assert msg_mock.call_args[0][0] \ - == '*Binance:* Selling [KEY/ETH]' \ - '(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \ + == '*Binance:* Selling KEY/ETH\n' \ '*Limit:* `0.00003201`\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00007500`\n' \ diff --git a/freqtrade/tests/rpc/test_rpc_webhook.py b/freqtrade/tests/rpc/test_rpc_webhook.py index 002308815..da7aec0a6 100644 --- a/freqtrade/tests/rpc/test_rpc_webhook.py +++ b/freqtrade/tests/rpc/test_rpc_webhook.py @@ -48,7 +48,6 @@ def test_send_msg(default_conf, mocker): 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', - 'market_url': "http://mockedurl/ETH_BTC", 'limit': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, @@ -73,7 +72,6 @@ def test_send_msg(default_conf, mocker): 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': "profit", - 'market_url': "http://mockedurl/ETH_BTC", 'limit': 0.005, 'amount': 0.8, 'open_rate': 0.004, @@ -127,7 +125,6 @@ def test_exception_send_msg(default_conf, mocker, caplog): 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', - 'market_url': "http://mockedurl/ETH_BTC", 'limit': 0.005, 'stake_amount': 0.8, 'stake_amount_fiat': 500, diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index a0ac6ee99..2f66a5153 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1872,7 +1872,6 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'profit', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', 'limit': 1.172e-05, 'amount': 90.99181073703367, 'open_rate': 1.099e-05, @@ -1919,7 +1918,6 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', 'limit': 1.044e-05, 'amount': 90.99181073703367, 'open_rate': 1.099e-05, @@ -1974,7 +1972,6 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', 'limit': 1.08801e-05, 'amount': 90.99181073703367, 'open_rate': 1.099e-05, @@ -2146,7 +2143,6 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'profit', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', 'limit': 1.172e-05, 'amount': 90.99181073703367, 'open_rate': 1.099e-05, @@ -2194,7 +2190,6 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', - 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', 'limit': 1.044e-05, 'amount': 90.99181073703367, 'open_rate': 1.099e-05, From ef264841534811fb7717152200b0e23d08947662 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Feb 2019 21:01:50 +0100 Subject: [PATCH 37/46] Super() should not be called with parameters source: https://realpython.com/python-super/ --- freqtrade/exchange/binance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 60d35f343..127f4e916 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -23,4 +23,4 @@ class Binance(Exchange): # get next-higher step in the limit_range list limit = min(list(filter(lambda x: limit <= x, limit_range))) - return super(Binance, self).get_order_book(pair, limit) + return super().get_order_book(pair, limit) From 4c2961f0d9c39fe1d7a477e50855bfa9f3aae27f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 27 Feb 2019 06:07:16 +0300 Subject: [PATCH 38/46] eliminate recursion in _detect_next_stop_or_sell_point() --- freqtrade/edge/__init__.py | 130 ++++++++++++++++++------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index baef811de..e95b4a3c4 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -351,7 +351,7 @@ class Edge(): return result def _detect_next_stop_or_sell_point(self, buy_column, sell_column, date_column, - ohlc_columns, stoploss, pair, start_point=0): + ohlc_columns, stoploss, pair): """ Iterate through ohlc_columns recursively in order to find the next trade Next trade opens from the first buy signal noticed to @@ -362,80 +362,80 @@ class Edge(): """ result: list = [] - open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal) + start_point = 0 - # return empty if we don't find trade entry (i.e. buy==1) or - # we find a buy but at the of array - if open_trade_index == -1 or open_trade_index == len(buy_column) - 1: - return [] - else: - open_trade_index += 1 # when a buy signal is seen, - # trade opens in reality on the next candle + while True: + open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal) - stop_price_percentage = stoploss + 1 - open_price = ohlc_columns[open_trade_index, 0] - stop_price = (open_price * stop_price_percentage) + # return empty if we don't find trade entry (i.e. buy==1) or + # we find a buy but at the of array + if open_trade_index == -1 or open_trade_index == len(buy_column) - 1: + break + else: + open_trade_index += 1 # when a buy signal is seen, + # trade opens in reality on the next candle - # Searching for the index where stoploss is hit - stop_index = utf1st.find_1st( - ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller) + stop_price_percentage = stoploss + 1 + open_price = ohlc_columns[open_trade_index, 0] + stop_price = (open_price * stop_price_percentage) - # If we don't find it then we assume stop_index will be far in future (infinite number) - if stop_index == -1: - stop_index = float('inf') + # Searching for the index where stoploss is hit + stop_index = utf1st.find_1st( + ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller) - # Searching for the index where sell is hit - sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal) + # If we don't find it then we assume stop_index will be far in future (infinite number) + if stop_index == -1: + stop_index = float('inf') - # If we don't find it then we assume sell_index will be far in future (infinite number) - if sell_index == -1: - sell_index = float('inf') + # Searching for the index where sell is hit + sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal) - # Check if we don't find any stop or sell point (in that case trade remains open) - # It is not interesting for Edge to consider it so we simply ignore the trade - # And stop iterating there is no more entry - if stop_index == sell_index == float('inf'): - return [] + # If we don't find it then we assume sell_index will be far in future (infinite number) + if sell_index == -1: + sell_index = float('inf') - if stop_index <= sell_index: - exit_index = open_trade_index + stop_index - exit_type = SellType.STOP_LOSS - exit_price = stop_price - elif stop_index > sell_index: - # if exit is SELL then we exit at the next candle - exit_index = open_trade_index + sell_index + 1 + # Check if we don't find any stop or sell point (in that case trade remains open) + # It is not interesting for Edge to consider it so we simply ignore the trade + # And stop iterating there is no more entry + if stop_index == sell_index == float('inf'): + break - # check if we have the next candle - if len(ohlc_columns) - 1 < exit_index: - return [] + if stop_index <= sell_index: + exit_index = open_trade_index + stop_index + exit_type = SellType.STOP_LOSS + exit_price = stop_price + elif stop_index > sell_index: + # if exit is SELL then we exit at the next candle + exit_index = open_trade_index + sell_index + 1 - exit_type = SellType.SELL_SIGNAL - exit_price = ohlc_columns[exit_index, 0] + # check if we have the next candle + if len(ohlc_columns) - 1 < exit_index: + break - trade = {'pair': pair, - 'stoploss': stoploss, - 'profit_percent': '', - 'profit_abs': '', - 'open_time': date_column[open_trade_index], - 'close_time': date_column[exit_index], - 'open_index': start_point + open_trade_index, - 'close_index': start_point + exit_index, - 'trade_duration': '', - 'open_rate': round(open_price, 15), - 'close_rate': round(exit_price, 15), - 'exit_type': exit_type - } + exit_type = SellType.SELL_SIGNAL + exit_price = ohlc_columns[exit_index, 0] - result.append(trade) + trade = {'pair': pair, + 'stoploss': stoploss, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': date_column[open_trade_index], + 'close_time': date_column[exit_index], + 'open_index': start_point + open_trade_index, + 'close_index': start_point + exit_index, + 'trade_duration': '', + 'open_rate': round(open_price, 15), + 'close_rate': round(exit_price, 15), + 'exit_type': exit_type + } - # Calling again the same function recursively but giving - # it a view of exit_index till the end of array - return result + self._detect_next_stop_or_sell_point( - buy_column[exit_index:], - sell_column[exit_index:], - date_column[exit_index:], - ohlc_columns[exit_index:], - stoploss, - pair, - (start_point + exit_index) - ) + result.append(trade) + + # giving a view of exit_index till the end of array + buy_column = buy_column[exit_index:] + sell_column = sell_column[exit_index:] + date_column = date_column[exit_index:] + ohlc_columns = ohlc_columns[exit_index:] + start_point += exit_index + + return result From 761861f0b7a87480f6af0e3837d3281ee430820b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 27 Feb 2019 13:35:06 +0300 Subject: [PATCH 39/46] comments: removed mentioning recursion, typos, etc. --- freqtrade/edge/__init__.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index e95b4a3c4..b4dfa5624 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -353,11 +353,12 @@ class Edge(): def _detect_next_stop_or_sell_point(self, buy_column, sell_column, date_column, ohlc_columns, stoploss, pair): """ - Iterate through ohlc_columns recursively in order to find the next trade + Iterate through ohlc_columns in order to find the next trade Next trade opens from the first buy signal noticed to The sell or stoploss signal after it. - It then calls itself cutting OHLC, buy_column, sell_colum and date_column - Cut from (the exit trade index) + 1 + It then cuts OHLC, buy_column, sell_column and date_column. + Cut from (the exit trade index) + 1. + Author: https://github.com/mishaker """ @@ -367,13 +368,14 @@ class Edge(): while True: open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal) - # return empty if we don't find trade entry (i.e. buy==1) or - # we find a buy but at the of array + # Return empty if we don't find trade entry (i.e. buy==1) or + # we find a buy but at the end of array if open_trade_index == -1 or open_trade_index == len(buy_column) - 1: break else: - open_trade_index += 1 # when a buy signal is seen, + # When a buy signal is seen, # trade opens in reality on the next candle + open_trade_index += 1 stop_price_percentage = stoploss + 1 open_price = ohlc_columns[open_trade_index, 0] @@ -405,10 +407,10 @@ class Edge(): exit_type = SellType.STOP_LOSS exit_price = stop_price elif stop_index > sell_index: - # if exit is SELL then we exit at the next candle + # If exit is SELL then we exit at the next candle exit_index = open_trade_index + sell_index + 1 - # check if we have the next candle + # Check if we have the next candle if len(ohlc_columns) - 1 < exit_index: break @@ -431,7 +433,7 @@ class Edge(): result.append(trade) - # giving a view of exit_index till the end of array + # Giving a view of exit_index till the end of array buy_column = buy_column[exit_index:] sell_column = sell_column[exit_index:] date_column = date_column[exit_index:] From 768f62a24a019c0cdb115308bc27beec0ffb922e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 27 Feb 2019 13:32:04 +0100 Subject: [PATCH 40/46] Update ccxt from 1.18.296 to 1.18.297 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ff62c1a29..dbdb41e72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.296 +ccxt==1.18.297 SQLAlchemy==1.2.18 python-telegram-bot==11.1.0 arrow==0.13.1 From 38d09f9e7853d34b0e7ebb354792db4dcdde5d89 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 27 Feb 2019 13:32:05 +0100 Subject: [PATCH 41/46] Update numpy from 1.16.1 to 1.16.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dbdb41e72..ac8707565 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ cachetools==3.1.0 requests==2.21.0 urllib3==1.24.1 wrapt==1.11.1 -numpy==1.16.1 +numpy==1.16.2 pandas==0.24.1 scikit-learn==0.20.2 joblib==0.13.2 From 13de66d559287326619a5fe5a58dd31befa845a2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 28 Feb 2019 13:32:06 +0100 Subject: [PATCH 42/46] Update ccxt from 1.18.297 to 1.18.304 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ac8707565..baff3ef9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.297 +ccxt==1.18.304 SQLAlchemy==1.2.18 python-telegram-bot==11.1.0 arrow==0.13.1 From e8ea2e6f05b50b91ad1eafe03dade853c9c6eda7 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 1 Mar 2019 13:32:06 +0100 Subject: [PATCH 43/46] Update ccxt from 1.18.304 to 1.18.309 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index baff3ef9c..e6b2f1b2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.304 +ccxt==1.18.309 SQLAlchemy==1.2.18 python-telegram-bot==11.1.0 arrow==0.13.1 From 0fc5445003acc3d9edf9ee585c4b1ba0feff9009 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 1 Mar 2019 13:32:07 +0100 Subject: [PATCH 44/46] Update jsonschema from 3.0.0 to 3.0.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e6b2f1b2e..8a7c95ddb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pandas==0.24.1 scikit-learn==0.20.2 joblib==0.13.2 scipy==1.2.1 -jsonschema==3.0.0 +jsonschema==3.0.1 TA-Lib==0.4.17 tabulate==0.8.3 coinmarketcap==5.0.3 From 28a70eba07976e81994141e697e6c8b6ac1cb377 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 2 Mar 2019 13:32:03 +0100 Subject: [PATCH 45/46] Update ccxt from 1.18.309 to 1.18.313 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8a7c95ddb..8cee146f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.309 +ccxt==1.18.313 SQLAlchemy==1.2.18 python-telegram-bot==11.1.0 arrow==0.13.1 From 6bcfe6587749e8cb9f205d862bfe596e93e94829 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 2 Mar 2019 13:32:04 +0100 Subject: [PATCH 46/46] Update scikit-learn from 0.20.2 to 0.20.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8cee146f5..a72198ac3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ urllib3==1.24.1 wrapt==1.11.1 numpy==1.16.2 pandas==0.24.1 -scikit-learn==0.20.2 +scikit-learn==0.20.3 joblib==0.13.2 scipy==1.2.1 jsonschema==3.0.1