diff --git a/.coveragerc b/.coveragerc index 95eea4f8f..4bd5b63fa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,4 +2,5 @@ omit = scripts/* freqtrade/tests/* - freqtrade/vendor/* \ No newline at end of file + freqtrade/vendor/* + freqtrade/__main__.py diff --git a/.travis.yml b/.travis.yml index 3f041f5dd..88121945f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,15 +16,16 @@ install: - pip install --upgrade flake8 coveralls pytest-random-order mypy - pip install -r requirements.txt - pip install -e . -- cp config.json.example config.json jobs: include: - script: - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ - coveralls - script: + - cp config.json.example config.json - python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting - script: + - cp config.json.example config.json - python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5 - script: flake8 freqtrade - script: mypy freqtrade diff --git a/README.md b/README.md index 24e01531c..94ebb7bf8 100644 --- a/README.md +++ b/README.md @@ -22,33 +22,10 @@ expect. We strongly recommend you to have coding and Python knowledge. Do not hesitate to read the source code and understand the mechanism of this bot. -## Table of Contents -- [Features](#features) -- [Quick start](#quick-start) -- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) - - [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) - - [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) - - [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) - - [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - - [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) -- [Support](#support) - - [Help](#help--slack) - - [Bugs](#bugs--issues) - - [Feature Requests](#feature-requests) - - [Pull Requests](#pull-requests) -- [Basic Usage](#basic-usage) - - [Bot commands](#bot-commands) - - [Telegram RPC commands](#telegram-rpc-commands) -- [Requirements](#requirements) - - [Min hardware required](#min-hardware-required) - - [Software requirements](#software-requirements) - -## Branches -The project is currently setup in two main branches: -- `develop` - This branch has often new features, but might also cause -breaking changes. -- `master` - This branch contains the latest stable release. The bot -'should' be stable on this branch, and is generally well tested. +## Exchange marketplaces supported +- [X] [Bittrex](https://bittrex.com/) +- [X] [Binance](https://www.binance.com/) +- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ## Features - [x] **Based on Python 3.6+**: For botting on any operating system - @@ -65,74 +42,50 @@ strategy parameters with real exchange data. - [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss. - [x] **Performance status report**: Provide a performance status of your current trades. -### Exchange marketplaces supported -- [X] [Bittrex](https://bittrex.com/) -- [X] [Binance](https://www.binance.com/) -- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ +## Table of Contents +- [Quick start](#quick-start) +- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) + - [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) + - [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) + - [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) + - [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) + - [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) +- [Basic Usage](#basic-usage) + - [Bot commands](#bot-commands) + - [Telegram RPC commands](#telegram-rpc-commands) +- [Support](#support) + - [Help](#help--slack) + - [Bugs](#bugs--issues) + - [Feature Requests](#feature-requests) + - [Pull Requests](#pull-requests) +- [Requirements](#requirements) + - [Min hardware required](#min-hardware-required) + - [Software requirements](#software-requirements) ## Quick start -This quick start section is a very short explanation on how to test the -bot in dry-run. We invite you to read the -[bot documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) -to ensure you understand how the bot is working. - -### Easy installation -The script below will install all dependencies and help you to configure the bot. -```bash -./setup.sh --install -``` - -### Manual installation -The following steps are made for Linux/MacOS environment - -**1. Clone the repo** +Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. ```bash git clone git@github.com:freqtrade/freqtrade.git git checkout develop cd freqtrade +./setup.sh --install ``` -**2. Create the config file** -Switch `"dry_run": true,` -```bash -cp config.json.example config.json -vi config.json -``` -**3. Build your docker image and run it** -```bash -docker build -t freqtrade . -docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` +_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_ -### Help / Slack -For any questions not covered by the documentation or for further -information about the bot, we encourage you to join our slack channel. -- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). +## Documentation +We invite you to read the bot documentation to ensure you understand how the bot is working. +- [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) +- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) +- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) +- [Bot usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md) + - [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands) + - [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands) + - [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands) +- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) +- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) +- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) -### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) -If you discover a bug in the bot, please -[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) -first. If it hasn't been reported, please -[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and -ensure you follow the template guide so that our team can assist you as -quickly as possible. - -### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) -Have you a great idea to improve the bot you want to share? Please, -first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement). -If it hasn't been requested, please -[create a new request](https://github.com/freqtrade/freqtrade/issues/new) -and ensure you follow the template guide so that it does not get lost -in the bug reports. - -### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls) -Feel like our bot is missing a feature? We welcome your pull requests! -Please read our -[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) -to understand the requirements before sending your pull-requests. - -**Important:** Always create your PR against the `develop` branch, not -`master`. ## Basic Usage @@ -170,11 +123,7 @@ optional arguments: "tradesv3.dry_run.sqlite" instead of memory DB. Work only if dry_run is enabled. ``` -More details on: -- [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands) -- [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands) -- [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands) - + ### Telegram RPC commands Telegram is not mandatory. However, this is a great way to control your bot. More details on our @@ -193,6 +142,46 @@ bot. More details on our - `/help`: Show help message - `/version`: Show version + +## Development branches +The project is currently setup in two main branches: +- `develop` - This branch has often new features, but might also cause +breaking changes. +- `master` - This branch contains the latest stable release. The bot +'should' be stable on this branch, and is generally well tested. + + +## Support +### Help / Slack +For any questions not covered by the documentation or for further +information about the bot, we encourage you to join our slack channel. +- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). + +### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) +If you discover a bug in the bot, please +[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) +first. If it hasn't been reported, please +[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and +ensure you follow the template guide so that our team can assist you as +quickly as possible. + +### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) +Have you a great idea to improve the bot you want to share? Please, +first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement). +If it hasn't been requested, please +[create a new request](https://github.com/freqtrade/freqtrade/issues/new) +and ensure you follow the template guide so that it does not get lost +in the bug reports. + +### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls) +Feel like our bot is missing a feature? We welcome your pull requests! +Please read our +[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) +to understand the requirements before sending your pull-requests. + +**Important:** Always create your PR against the `develop` branch, not +`master`. + ## Requirements ### Min hardware required diff --git a/config.json.example b/config.json.example index d3dbeb52e..e5bdc95b1 100644 --- a/config.json.example +++ b/config.json.example @@ -31,7 +31,8 @@ }, "experimental": { "use_sell_signal": false, - "sell_profit_only": false + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false }, "telegram": { "enabled": true, diff --git a/config_full.json.example b/config_full.json.example index c17d22a15..231384acc 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -38,7 +38,8 @@ }, "experimental": { "use_sell_signal": false, - "sell_profit_only": false + "sell_profit_only": false, + "ignore_roi_if_buy_signal": false }, "telegram": { "enabled": true, diff --git a/docs/configuration.md b/docs/configuration.md index 9c76faa28..5279ed06e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -31,6 +31,7 @@ The table below will list all configuration parameters. | `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. | `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`. | `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision. +| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal` | `telegram.enabled` | true | Yes | Enable or not the usage of Telegram. | `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. diff --git a/docs/installation.md b/docs/installation.md index b6688885b..e5724a7dc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -8,6 +8,7 @@ To understand how to set up the bot please read the [Bot Configuration](https:// * [Table of Contents](#table-of-contents) * [Easy Installation - Linux Script](#easy-installation---linux-script) +* [Manual installation](#manual-installation) * [Automatic Installation - Docker](#automatic-installation---docker) * [Custom Linux MacOS Installation](#custom-installation) - [Requirements](#requirements) @@ -55,6 +56,28 @@ Reset parameter will hard reset your branch (only if you are on `master` or `dev Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. + +## Manual installation - Linux/MacOS +The following steps are made for Linux/MacOS environment + +**1. Clone the repo** +```bash +git clone git@github.com:freqtrade/freqtrade.git +git checkout develop +cd freqtrade +``` +**2. Create the config file** +Switch `"dry_run": true,` +```bash +cp config.json.example config.json +vi config.json +``` +**3. Build your docker image and run it** +```bash +docker build -t freqtrade . +docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + ------ ## Automatic Installation - Docker @@ -184,6 +207,26 @@ docker start freqtrade You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. +### 7. Backtest with docker + +The following assumes that the above steps (1-4) have been completed successfully. +Also, backtest-data should be available at `~/.freqtrade/user_data/`. + + +``` bash +docker run -d \ + --name freqtrade \ + -v /etc/localtime:/etc/localtime:ro \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ + freqtrade --strategy AwsomelyProfitableStrategy backtesting +``` + +Head over to the [Backtesting Documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) for more details. + +*Note*: Additional parameters can be appended after the image name (`freqtrade` in the above example). + ------ ## Custom Installation diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 2ef3a7515..36e00dd0e 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -10,7 +10,7 @@ import arrow from pandas import DataFrame, to_datetime from freqtrade import constants -from freqtrade.exchange import get_ticker_history +from freqtrade.exchange import Exchange from freqtrade.persistence import Trade from freqtrade.strategy.resolver import StrategyResolver, IStrategy @@ -117,14 +117,14 @@ class Analyze(object): dataframe = self.populate_sell_trend(dataframe) return dataframe - def get_signal(self, pair: str, interval: str) -> Tuple[bool, bool]: + def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool, bool]: """ Calculates current signal based several technical analysis indicators :param pair: pair in format ANT/BTC :param interval: Interval to use (in min) :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ - ticker_hist = get_ticker_history(pair, interval) + ticker_hist = exchange.get_ticker_history(pair, interval) if not ticker_hist: logger.warning('Empty ticker history for pair %s', pair) return False, False @@ -155,7 +155,7 @@ class Analyze(object): # Check if dataframe is out of date signal_date = arrow.get(latest['date']) interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] - if signal_date < arrow.utcnow() - timedelta(minutes=(interval_minutes + 5)): + if signal_date < (arrow.utcnow() - timedelta(minutes=(interval_minutes + 5))): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', pair, @@ -179,33 +179,45 @@ class Analyze(object): if the threshold is reached and updates the trade record. :return: True if trade should be sold, False otherwise """ + current_profit = trade.calc_profit_percent(rate) + if self.stop_loss_reached(current_profit=current_profit): + return True + + experimental = self.config.get('experimental', {}) + + if buy and experimental.get('ignore_roi_if_buy_signal', False): + logger.debug('Buy signal still active - not selling.') + return False + # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) - if self.min_roi_reached(trade=trade, current_rate=rate, current_time=date): + if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date): logger.debug('Required profit reached. Selling..') return True - # Experimental: Check if the trade is profitable before selling it (avoid selling at loss) - if self.config.get('experimental', {}).get('sell_profit_only', False): + if experimental.get('sell_profit_only', False): logger.debug('Checking if trade is profitable..') if trade.calc_profit(rate=rate) <= 0: return False - - if sell and not buy and self.config.get('experimental', {}).get('use_sell_signal', False): + if sell and not buy and experimental.get('use_sell_signal', False): logger.debug('Sell signal received. Selling..') return True return False - def min_roi_reached(self, trade: Trade, current_rate: float, current_time: datetime) -> bool: + def stop_loss_reached(self, current_profit: float) -> bool: + """Based on current profit of the trade and configured stoploss, decides to sell or not""" + + if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss: + logger.debug('Stop loss hit.') + return True + return False + + def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: """ Based an earlier trade and current price and ROI configuration, decides whether bot should sell :return True if bot should sell at current rate """ - current_profit = trade.calc_profit_percent(current_rate) - if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss: - logger.debug('Stop loss hit.') - return True # Check if time matches and current rate is above threshold time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60 diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 30d7111dc..0f12905e3 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -79,7 +79,8 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'use_sell_signal': {'type': 'boolean'}, - 'sell_profit_only': {'type': 'boolean'} + 'sell_profit_only': {'type': 'boolean'}, + "ignore_roi_if_buy_signal_true": {'type': 'boolean'} } }, 'telegram': { diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 54d564f04..acfefdad4 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -12,16 +12,8 @@ from freqtrade import constants, OperationalException, DependencyException, Temp logger = logging.getLogger(__name__) -# Current selected exchange -_API: ccxt.Exchange = None - -_CONF: Dict = {} API_RETRY_COUNT = 4 -_CACHED_TICKER: Dict[str, Any] = {} - -# Holds all open sell orders for dry_run -_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {} # Urls to exchange markets, insert quote and base with .format() _EXCHANGE_URLS = { @@ -48,390 +40,378 @@ def retrier(f): return wrapper -def init_ccxt(exchange_config: dict) -> ccxt.Exchange: - """ - Initialize ccxt with given config and return valid - ccxt instance. - :param config: config to use - :return: ccxt - """ - # Find matching class for the given exchange name - name = exchange_config['name'] +class Exchange(object): - if name not in ccxt.exchanges: - raise OperationalException(f'Exchange {name} is not supported') - try: - api = getattr(ccxt, name.lower())({ - 'apiKey': exchange_config.get('key'), - 'secret': exchange_config.get('secret'), - 'password': exchange_config.get('password'), - 'uid': exchange_config.get('uid', ''), - 'enableRateLimit': True, - }) - except (KeyError, AttributeError): - raise OperationalException(f'Exchange {name} is not supported') + # Current selected exchange + _api: ccxt.Exchange = None + _conf: Dict = {} + _cached_ticker: Dict[str, Any] = {} - return api + # Holds all open sell orders for dry_run + _dry_run_open_orders: Dict[str, Any] = {} + 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. + :return: None + """ + self._conf.update(config) -def init(config: dict) -> None: - """ - Initializes this module with the given config, - it does basic validation whether the specified - exchange and pairs are valid. - :param config: config to use - :return: None - """ - global _CONF, _API + if config['dry_run']: + logger.info('Instance is running with dry_run enabled') - _CONF.update(config) + exchange_config = config['exchange'] + self._api = self._init_ccxt(exchange_config) - if config['dry_run']: - logger.info('Instance is running with dry_run enabled') + logger.info('Using Exchange "%s"', self.name) - exchange_config = config['exchange'] - _API = init_ccxt(exchange_config) + # Check if all pairs are available + self.validate_pairs(config['exchange']['pair_whitelist']) - logger.info('Using Exchange "%s"', get_name()) + def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange: + """ + Initialize ccxt with given config and return valid + ccxt instance. + """ + # Find matching class for the given exchange name + name = exchange_config['name'] - # Check if all pairs are available - validate_pairs(config['exchange']['pair_whitelist']) - - -def validate_pairs(pairs: List[str]) -> None: - """ - Checks if all given pairs are tradable on the current exchange. - Raises OperationalException if one pair is not available. - :param pairs: list of pairs - :return: None - """ - - try: - markets = _API.load_markets() - except ccxt.BaseError as e: - logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e) - return - - stake_cur = _CONF['stake_currency'] - for pair in pairs: - # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs - # TODO: add a support for having coins in BTC/USDT format - if not pair.endswith(stake_cur): - raise OperationalException( - f'Pair {pair} not compatible with stake_currency: {stake_cur}') - if pair not in markets: - raise OperationalException( - f'Pair {pair} is not available at {get_name()}') - - -def exchange_has(endpoint: str) -> bool: - """ - Checks if exchange implements a specific API endpoint. - Wrapper around ccxt 'has' attribute - :param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers') - :return: bool - """ - return endpoint in _API.has and _API.has[endpoint] - - -def buy(pair: str, rate: float, amount: float) -> Dict: - if _CONF['dry_run']: - global _DRY_RUN_OPEN_ORDERS - order_id = f'dry_run_buy_{randint(0, 10**6)}' - _DRY_RUN_OPEN_ORDERS[order_id] = { - 'pair': pair, - 'price': rate, - 'amount': amount, - 'type': 'limit', - 'side': 'buy', - 'remaining': 0.0, - 'datetime': arrow.utcnow().isoformat(), - 'status': 'closed', - 'fee': None - } - return {'id': order_id} - - try: - return _API.create_limit_buy_order(pair, amount, rate) - 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) - - -def sell(pair: str, rate: float, amount: float) -> Dict: - if _CONF['dry_run']: - global _DRY_RUN_OPEN_ORDERS - order_id = f'dry_run_sell_{randint(0, 10**6)}' - _DRY_RUN_OPEN_ORDERS[order_id] = { - 'pair': pair, - 'price': rate, - 'amount': amount, - 'type': 'limit', - 'side': 'sell', - 'remaining': 0.0, - 'datetime': arrow.utcnow().isoformat(), - 'status': 'closed' - } - return {'id': order_id} - - try: - return _API.create_limit_sell_order(pair, amount, rate) - 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) - - -@retrier -def get_balance(currency: str) -> float: - if _CONF['dry_run']: - return 999.9 - - # ccxt exception is already handled by get_balances - balances = get_balances() - balance = balances.get(currency) - if balance is None: - raise TemporaryError( - f'Could not get {currency} balance due to malformed exchange response: {balances}') - return balance['free'] - - -@retrier -def get_balances() -> dict: - if _CONF['dry_run']: - return {} - - try: - balances = _API.fetch_balance() - # Remove additional info from ccxt results - balances.pop("info", None) - balances.pop("free", None) - balances.pop("total", None) - balances.pop("used", None) - - return balances - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get balance due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -@retrier -def get_tickers() -> Dict: - try: - return _API.fetch_tickers() - except ccxt.NotSupported as e: - raise OperationalException( - f'Exchange {_API.name} does not support fetching tickers in batch.' - f'Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -@retrier -def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: - global _CACHED_TICKER - if refresh or pair not in _CACHED_TICKER.keys(): + if name not in ccxt.exchanges: + raise OperationalException(f'Exchange {name} is not supported') try: - data = _API.fetch_ticker(pair) + api = getattr(ccxt, name.lower())({ + 'apiKey': exchange_config.get('key'), + 'secret': exchange_config.get('secret'), + 'password': exchange_config.get('password'), + 'uid': exchange_config.get('uid', ''), + 'enableRateLimit': True, + }) + except (KeyError, AttributeError): + raise OperationalException(f'Exchange {name} is not supported') + + return api + + @property + def name(self) -> str: + """exchange Name (from ccxt)""" + return self._api.name + + @property + def id(self) -> str: + """exchange ccxt id""" + return self._api.id + + def validate_pairs(self, pairs: List[str]) -> None: + """ + Checks if all given pairs are tradable on the current exchange. + Raises OperationalException if one pair is not available. + :param pairs: list of pairs + :return: None + """ + + try: + markets = self._api.load_markets() + except ccxt.BaseError as e: + logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e) + return + + stake_cur = self._conf['stake_currency'] + for pair in pairs: + # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs + # TODO: add a support for having coins in BTC/USDT format + if not pair.endswith(stake_cur): + raise OperationalException( + f'Pair {pair} not compatible with stake_currency: {stake_cur}') + if pair not in markets: + raise OperationalException( + f'Pair {pair} is not available at {self.name}') + + def exchange_has(self, endpoint: str) -> bool: + """ + Checks if exchange implements a specific API endpoint. + Wrapper around ccxt 'has' attribute + :param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers') + :return: bool + """ + return endpoint in self._api.has and self._api.has[endpoint] + + def buy(self, pair: str, rate: float, amount: float) -> 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': 'limit', + 'side': 'buy', + 'remaining': 0.0, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'closed', + 'fee': None + } + return {'id': order_id} + + try: + return self._api.create_limit_buy_order(pair, amount, rate) + 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) + + def sell(self, pair: str, rate: float, amount: float) -> 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': 'limit', + 'side': 'sell', + 'remaining': 0.0, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'closed' + } + return {'id': order_id} + + try: + return self._api.create_limit_sell_order(pair, amount, rate) + 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) + + @retrier + def get_balance(self, currency: str) -> float: + if self._conf['dry_run']: + return 999.9 + + # ccxt exception is already handled by get_balances + balances = self.get_balances() + balance = balances.get(currency) + if balance is None: + raise TemporaryError( + f'Could not get {currency} balance due to malformed exchange response: {balances}') + return balance['free'] + + @retrier + def get_balances(self) -> dict: + if self._conf['dry_run']: + return {} + + try: + balances = self._api.fetch_balance() + # Remove additional info from ccxt results + balances.pop("info", None) + balances.pop("free", None) + balances.pop("total", None) + balances.pop("used", None) + + return balances + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get balance due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) + + @retrier + def get_tickers(self) -> Dict: + try: + return self._api.fetch_tickers() + except ccxt.NotSupported as e: + raise OperationalException( + f'Exchange {self._api.name} does not support fetching tickers in batch.' + f'Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) + + @retrier + def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: + if refresh or pair not in self._cached_ticker.keys(): try: - _CACHED_TICKER[pair] = { - 'bid': float(data['bid']), - 'ask': float(data['ask']), - } - except KeyError: - logger.debug("Could not cache ticker data for %s", pair) + data = self._api.fetch_ticker(pair) + try: + self._cached_ticker[pair] = { + 'bid': float(data['bid']), + 'ask': float(data['ask']), + } + except KeyError: + logger.debug("Could not cache ticker data for %s", pair) + return data + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) + else: + logger.info("returning cached ticker-data for %s", pair) + return self._cached_ticker[pair] + + @retrier + def get_ticker_history(self, pair: str, tick_interval: str, + since_ms: Optional[int] = None) -> List[Dict]: + try: + # last item should be in the time interval [now - tick_interval, now] + till_time_ms = arrow.utcnow().shift( + minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval] + ).timestamp * 1000 + # it looks as if some exchanges return cached data + # and they update it one in several minute, so 10 mins interval + # is necessary to skeep downloading of an empty array when all + # chached data was already downloaded + till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000) + + data: List[Dict[Any, Any]] = [] + while not since_ms or since_ms < till_time_ms: + data_part = self._api.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms) + + # Because some exchange sort Tickers ASC and other DESC. + # Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) + # when GDAX returns a list of tickers DESC (newest first, oldest last) + data_part = sorted(data_part, key=lambda x: x[0]) + + if not data_part: + break + + logger.debug('Downloaded data for %s time range [%s, %s]', + pair, + arrow.get(data_part[0][0] / 1000).format(), + arrow.get(data_part[-1][0] / 1000).format()) + + data.extend(data_part) + since_ms = data[-1][0] + 1 + return data + except ccxt.NotSupported as e: + raise OperationalException( + f'Exchange {self._api.name} does not support fetching historical candlestick data.' + f'Message: {e}') except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(f'Could not fetch ticker data. Msg: {e}') + + @retrier + def cancel_order(self, order_id: str, pair: str) -> None: + if self._conf['dry_run']: + return + + try: + return self._api.cancel_order(order_id, pair) + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not cancel order. Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') except ccxt.BaseError as e: raise OperationalException(e) - else: - logger.info("returning cached ticker-data for %s", pair) - return _CACHED_TICKER[pair] + @retrier + 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) + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not get order. Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get order due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) -@retrier -def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] = None) -> List[Dict]: - try: - # last item should be in the time interval [now - tick_interval, now] - till_time_ms = arrow.utcnow().shift( - minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval] - ).timestamp * 1000 - # it looks as if some exchanges return cached data - # and they update it one in several minute, so 10 mins interval - # is necessary to skeep downloading of an empty array when all - # chached data was already downloaded - till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000) + @retrier + def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: + if self._conf['dry_run']: + return [] + if not self.exchange_has('fetchMyTrades'): + return [] + try: + my_trades = self._api.fetch_my_trades(pair, since.timestamp()) + matched_trades = [trade for trade in my_trades if trade['order'] == order_id] - data: List[Dict[Any, Any]] = [] - while not since_ms or since_ms < till_time_ms: - data_part = _API.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms) + return matched_trades - # Because some exchange sort Tickers ASC and other DESC. - # Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) - # when GDAX returns a list of tickers DESC (newest first, oldest last) - data_part = sorted(data_part, key=lambda x: x[0]) + except ccxt.NetworkError as e: + raise TemporaryError( + f'Could not get trades due to networking error. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) - if not data_part: - break + def get_pair_detail_url(self, pair: str) -> str: + try: + url_base = self._api.urls.get('www') + base, quote = pair.split('/') - logger.debug('Downloaded data for %s time range [%s, %s]', - pair, - arrow.get(data_part[0][0] / 1000).format(), - arrow.get(data_part[-1][0] / 1000).format()) + 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 "" - data.extend(data_part) - since_ms = data[-1][0] + 1 + @retrier + def get_markets(self) -> List[dict]: + try: + return self._api.fetch_markets() + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not load markets due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) - return data - except ccxt.NotSupported as e: - raise OperationalException( - f'Exchange {_API.name} does not support fetching historical candlestick data.' - f'Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(f'Could not fetch ticker data. Msg: {e}') + @retrier + def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1, + price=1, taker_or_maker='maker') -> float: + try: + # validate that markets are loaded before trying to get fee + if self._api.markets is None or len(self._api.markets) == 0: + self._api.load_markets() + return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, + price=price, takerOrMaker=taker_or_maker)['rate'] + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) -@retrier -def cancel_order(order_id: str, pair: str) -> None: - if _CONF['dry_run']: - return - - try: - return _API.cancel_order(order_id, pair) - except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not cancel order. Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -@retrier -def get_order(order_id: str, pair: str) -> Dict: - if _CONF['dry_run']: - order = _DRY_RUN_OPEN_ORDERS[order_id] - order.update({ - 'id': order_id - }) - return order - try: - return _API.fetch_order(order_id, pair) - except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not get order. Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get order due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -@retrier -def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List: - if _CONF['dry_run']: - return [] - if not exchange_has('fetchMyTrades'): - return [] - try: - my_trades = _API.fetch_my_trades(pair, since.timestamp()) - matched_trades = [trade for trade in my_trades if trade['order'] == order_id] - - return matched_trades - - except ccxt.NetworkError as e: - raise TemporaryError( - f'Could not get trades due to networking error. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -def get_pair_detail_url(pair: str) -> str: - try: - url_base = _API.urls.get('www') - base, quote = pair.split('/') - - return url_base + _EXCHANGE_URLS[_API.id].format(base=base, quote=quote) - except KeyError: - logger.warning('Could not get exchange url for %s', get_name()) - return "" - - -@retrier -def get_markets() -> List[dict]: - try: - return _API.fetch_markets() - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load markets due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -def get_name() -> str: - return _API.name - - -def get_id() -> str: - return _API.id - - -@retrier -def get_fee(symbol='ETH/BTC', type='', side='', amount=1, - price=1, taker_or_maker='maker') -> float: - try: + def get_amount_lots(self, pair: str, amount: float) -> float: + """ + get buyable amount rounding, .. + """ # validate that markets are loaded before trying to get fee - if _API.markets is None or len(_API.markets) == 0: - _API.load_markets() - - return _API.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, - price=price, takerOrMaker=taker_or_maker)['rate'] - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -def get_amount_lots(pair: str, amount: float) -> float: - """ - get buyable amount rounding, .. - """ - # validate that markets are loaded before trying to get fee - if not _API.markets: - _API.load_markets() - return _API.amount_to_lots(pair, amount) + if not self._api.markets: + self._api.load_markets() + return self._api.amount_to_lots(pair, amount) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a5612371d..e25ed66cf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -14,11 +14,11 @@ import requests from cachetools import TTLCache, cached from freqtrade import ( - DependencyException, OperationalException, TemporaryError, - exchange, persistence, __version__, + DependencyException, OperationalException, TemporaryError, persistence, __version__, ) from freqtrade import constants from freqtrade.analyze import Analyze +from freqtrade.exchange import Exchange from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.persistence import Trade from freqtrade.rpc.rpc_manager import RPCManager @@ -54,7 +54,7 @@ class FreqtradeBot(object): self.fiat_converter = CryptoToFiatConverter() self.rpc: RPCManager = RPCManager(self) self.persistence = None - self.exchange = None + self.exchange = Exchange(self.config) self._init_modules() @@ -66,7 +66,6 @@ class FreqtradeBot(object): # Initialize all modules persistence.init(self.config) - exchange.init(self.config) # Set initial application state initial_state = self.config.get('initial_state') @@ -186,13 +185,13 @@ class FreqtradeBot(object): :return: List of pairs """ - if not exchange.exchange_has('fetchTickers'): + if not self.exchange.exchange_has('fetchTickers'): raise OperationalException( 'Exchange does not support dynamic whitelist.' 'Please edit your config and restart the bot' ) - tickers = exchange.get_tickers() + tickers = self.exchange.get_tickers() # check length so that we make sure that '/' is actually in the string tickers = [v for k, v in tickers.items() if len(k.split('/')) == 2 and k.split('/')[1] == base_currency] @@ -210,7 +209,7 @@ class FreqtradeBot(object): black_listed """ sanitized_whitelist = whitelist - markets = exchange.get_markets() + markets = self.exchange.get_markets() markets = [m for m in markets if m['quote'] == self.config['stake_currency']] known_pairs = set() @@ -247,7 +246,7 @@ class FreqtradeBot(object): def _get_trade_stake_amount(self) -> Optional[float]: stake_amount = self.config['stake_amount'] - avaliable_amount = exchange.get_balance(self.config['stake_currency']) + avaliable_amount = self.exchange.get_balance(self.config['stake_currency']) if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all()) @@ -267,7 +266,7 @@ class FreqtradeBot(object): return stake_amount def _get_min_pair_stake_amount(self, pair: str, price: float) -> Optional[float]: - markets = exchange.get_markets() + markets = self.exchange.get_markets() markets = [m for m in markets if m['symbol'] == pair] if not markets: raise ValueError(f'Can\'t get market information for symbol {pair}') @@ -307,7 +306,7 @@ class FreqtradeBot(object): return False stake_currency = self.config['stake_currency'] fiat_currency = self.config['fiat_display_currency'] - exc_name = exchange.get_name() + exc_name = self.exchange.name logger.info( 'Checking buy signals to create a new trade with stake_amount: %f ...', @@ -326,16 +325,17 @@ class FreqtradeBot(object): # Pick pair based on buy signals for _pair in whitelist: - (buy, sell) = self.analyze.get_signal(_pair, interval) + (buy, sell) = self.analyze.get_signal(self.exchange, _pair, interval) if buy and not sell: pair = _pair break else: return False pair_s = pair.replace('_', '/') - pair_url = exchange.get_pair_detail_url(pair) + pair_url = self.exchange.get_pair_detail_url(pair) + # Calculate amount - buy_limit = self.get_target_bid(exchange.get_ticker(pair)) + buy_limit = self.get_target_bid(self.exchange.get_ticker(pair)) min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit) if min_stake_amount is not None and min_stake_amount > stake_amount: @@ -346,7 +346,8 @@ class FreqtradeBot(object): return False amount = stake_amount / buy_limit - order_id = exchange.buy(pair, buy_limit, amount)['id'] + + order_id = self.exchange.buy(pair, buy_limit, amount)['id'] stake_amount_fiat = self.fiat_converter.convert_amount( stake_amount, @@ -361,7 +362,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ {stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`""" ) # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL - fee = exchange.get_fee(symbol=pair, taker_or_maker='maker') + fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( pair=pair, stake_amount=stake_amount, @@ -371,7 +372,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ open_rate=buy_limit, open_rate_requested=buy_limit, open_date=datetime.utcnow(), - exchange=exchange.get_id(), + exchange=self.exchange.id, open_order_id=order_id ) Trade.session.add(trade) @@ -404,7 +405,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ if trade.open_order_id: # Update trade with order values logger.info('Found open order for %s', trade) - order = exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.get_order(trade.open_order_id, trade.pair) # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) @@ -428,7 +429,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ def get_real_amount(self, trade: Trade, order: Dict) -> float: """ Get real amount for the trade - Necessary for exchanges which charge fees in base currency (e.g. binance) + Necessary for self.exchanges which charge fees in base currency (e.g. binance) """ order_amount = order['amount'] # Only run for closed orders @@ -444,7 +445,8 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ return new_amount # Fallback to Trades - trades = exchange.get_trades_for_order(trade.open_order_id, trade.pair, trade.open_date) + trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair, + trade.open_date) if len(trades) == 0: logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) @@ -476,12 +478,13 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ raise ValueError(f'attempt to handle closed trade: {trade}') logger.debug('Handling %s ...', trade) - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = self.exchange.get_ticker(trade.pair)['bid'] (buy, sell) = (False, False) - - if self.config.get('experimental', {}).get('use_sell_signal'): - (buy, sell) = self.analyze.get_signal(trade.pair, self.analyze.get_ticker_interval()) + experimental = self.config.get('experimental', {}) + if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'): + (buy, sell) = self.analyze.get_signal(self.exchange, + trade.pair, self.analyze.get_ticker_interval()) if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell): self.execute_sell(trade, current_rate) @@ -505,7 +508,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ # updated via /forcesell in a different thread. if not trade.open_order_id: continue - order = exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.get_order(trade.open_order_id, trade.pair) except requests.exceptions.RequestException: logger.info( 'Cannot query order for %s due to %s', @@ -531,7 +534,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ :return: True if order was fully cancelled """ pair_s = trade.pair.replace('_', '/') - exchange.cancel_order(trade.open_order_id, trade.pair) + self.exchange.cancel_order(trade.open_order_id, trade.pair) if order['remaining'] == order['amount']: # if trade is not partially completed, just delete the trade Trade.session.delete(trade) @@ -558,7 +561,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ pair_s = trade.pair.replace('_', '/') if order['remaining'] == order['amount']: # if trade is not partially completed, just cancel the trade - exchange.cancel_order(trade.open_order_id, trade.pair) + self.exchange.cancel_order(trade.open_order_id, trade.pair) trade.close_rate = None trade.close_profit = None trade.close_date = None @@ -581,15 +584,15 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ exc = trade.exchange pair = trade.pair # Execute sell and update trade record - order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id'] + order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id'] trade.open_order_id = order_id trade.close_rate_requested = limit fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2) profit_trade = trade.calc_profit(rate=limit) - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = self.exchange.get_ticker(trade.pair)['bid'] profit = trade.calc_profit_percent(limit) - pair_url = exchange.get_pair_detail_url(trade.pair) + pair_url = self.exchange.get_pair_detail_url(trade.pair) gain = "profit" if fmt_exp_profit > 0 else "loss" message = f"*{exc}:* Selling\n" \ diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 867e8c7dc..1e76808e7 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -7,8 +7,8 @@ import os from typing import Optional, List, Dict, Tuple, Any import arrow -from freqtrade import misc, constants -from freqtrade.exchange import get_ticker_history +from freqtrade import misc, constants, OperationalException +from freqtrade.exchange import Exchange from freqtrade.arguments import TimeRange logger = logging.getLogger(__name__) @@ -83,6 +83,7 @@ def load_data(datadir: str, ticker_interval: str, pairs: List[str], refresh_pairs: Optional[bool] = False, + exchange: Optional[Exchange] = None, timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]: """ Loads ticker history data for the given parameters @@ -93,7 +94,10 @@ def load_data(datadir: str, # If the user force the refresh of pairs if refresh_pairs: logger.info('Download data for all pairs and store them in %s', datadir) - download_pairs(datadir, pairs, ticker_interval, timerange=timerange) + if not exchange: + raise OperationalException("Exchange needs to be initialized when " + "calling load_data with refresh_pairs=True") + download_pairs(datadir, exchange, pairs, ticker_interval, timerange=timerange) for pair in pairs: pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) @@ -119,13 +123,14 @@ def make_testdata_path(datadir: str) -> str: ) -def download_pairs(datadir, pairs: List[str], +def download_pairs(datadir, exchange: Exchange, pairs: List[str], ticker_interval: str, timerange: TimeRange = TimeRange(None, None, 0, 0)) -> bool: """For each pairs passed in parameters, download the ticker intervals""" for pair in pairs: try: download_backtesting_testdata(datadir, + exchange=exchange, pair=pair, tick_interval=ticker_interval, timerange=timerange) @@ -183,6 +188,7 @@ def load_cached_data_for_updating(filename: str, def download_backtesting_testdata(datadir: str, + exchange: Exchange, pair: str, tick_interval: str = '5m', timerange: Optional[TimeRange] = None) -> None: @@ -216,7 +222,8 @@ def download_backtesting_testdata(datadir: str, logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') - new_data = get_ticker_history(pair=pair, tick_interval=tick_interval, since_ms=since_ms) + new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval, + since_ms=since_ms) data.extend(new_data) logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f453fbb47..6982b36cb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -14,7 +14,8 @@ from pandas import DataFrame from tabulate import tabulate import freqtrade.optimize as optimize -from freqtrade import exchange, constants, DependencyException +from freqtrade import constants, DependencyException +from freqtrade.exchange import Exchange from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration @@ -61,7 +62,8 @@ class Backtesting(object): self.config['exchange']['password'] = '' self.config['exchange']['uid'] = '' self.config['dry_run'] = True - exchange.init(self.config) + self.exchange = Exchange(self.config) + self.fee = self.exchange.get_fee() @staticmethod def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: @@ -130,14 +132,13 @@ class Backtesting(object): stake_amount = args['stake_amount'] max_open_trades = args.get('max_open_trades', 0) - fee = exchange.get_fee() trade = Trade( open_rate=buy_row.close, open_date=buy_row.date, stake_amount=stake_amount, amount=stake_amount / buy_row.open, - fee_open=fee, - fee_close=fee + fee_open=self.fee, + fee_close=self.fee ) # calculate win/lose forwards from buy point @@ -256,7 +257,7 @@ class Backtesting(object): if self.config.get('live'): logger.info('Downloading data for all pairs in whitelist ...') for pair in pairs: - data[pair] = exchange.get_ticker_history(pair, self.ticker_interval) + data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval) else: logger.info('Using local backtesting data (using whitelist in given config) ...') @@ -267,6 +268,7 @@ class Backtesting(object): pairs=pairs, ticker_interval=self.ticker_interval, refresh_pairs=self.config.get('refresh_pairs', False), + exchange=self.exchange, timerange=timerange ) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 19e732d7b..7a313a3ac 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -39,7 +39,6 @@ class Hyperopt(Backtesting): hyperopt.start() """ def __init__(self, config: Dict[str, Any]) -> None: - super().__init__(config) # set TARGET_TRADES to suit your number concurrent trades so its realistic # to the number of days diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 7fd8fdeb9..20a7db4bb 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -21,7 +21,6 @@ from freqtrade import OperationalException logger = logging.getLogger(__name__) -_CONF = {} _DECL_BASE: Any = declarative_base() @@ -33,9 +32,7 @@ def init(config: Dict) -> None: :param config: config to use :return: None """ - _CONF.update(config) - - db_url = _CONF.get('db_url', None) + db_url = config.get('db_url', None) kwargs = {} # Take care of thread ownership if in-memory db @@ -61,7 +58,7 @@ def init(config: Dict) -> None: check_migrate(engine) # Clean dry_run DB if the db is not in-memory - if _CONF.get('dry_run', False) and db_url != 'sqlite://': + if config.get('dry_run', False) and db_url != 'sqlite://': clean_dry_run_db() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 34802f920..ee6ecb770 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -12,7 +12,6 @@ import sqlalchemy as sql from numpy import mean, nan_to_num from pandas import DataFrame -from freqtrade import exchange from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.state import State @@ -71,9 +70,9 @@ class RPC(object): for trade in trades: order = None if trade.open_order_id: - order = exchange.get_order(trade.open_order_id, trade.pair) + order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) # calculate profit and send message to user - current_rate = exchange.get_ticker(trade.pair, False)['bid'] + current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_profit = trade.calc_profit_percent(current_rate) fmt_close_profit = '{:.2f}%'.format( round(trade.close_profit * 100, 2) @@ -91,7 +90,7 @@ class RPC(object): .format( trade_id=trade.id, pair=trade.pair, - market_url=exchange.get_pair_detail_url(trade.pair), + market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair), date=arrow.get(trade.open_date).humanize(), open_rate=trade.open_rate, close_rate=trade.close_rate, @@ -116,7 +115,7 @@ class RPC(object): trades_list = [] for trade in trades: # calculate profit and send message to user - current_rate = exchange.get_ticker(trade.pair, False)['bid'] + current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] trades_list.append([ trade.id, trade.pair, @@ -201,7 +200,7 @@ class RPC(object): profit_closed_percent.append(profit_percent) else: # Get current rate - current_rate = exchange.get_ticker(trade.pair, False)['bid'] + current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] profit_percent = trade.calc_profit_percent(rate=current_rate) profit_all_coin.append( @@ -258,7 +257,7 @@ class RPC(object): """ Returns current account balance per crypto """ output = [] total = 0.0 - for coin, balance in exchange.get_balances().items(): + for coin, balance in self._freqtrade.exchange.get_balances().items(): if not balance['total']: continue @@ -266,9 +265,9 @@ class RPC(object): rate = 1.0 else: if coin == 'USDT': - rate = 1.0 / exchange.get_ticker('BTC/USDT', False)['bid'] + rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid'] else: - rate = exchange.get_ticker(coin + '/BTC', False)['bid'] + rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid'] est_btc: float = rate * balance['total'] total = total + est_btc output.append( @@ -318,13 +317,13 @@ class RPC(object): def _exec_forcesell(trade: Trade) -> None: # Check if there is there is an open order if trade.open_order_id: - order = exchange.get_order(trade.open_order_id, trade.pair) + order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) # Cancel open LIMIT_BUY orders and close trade if order and order['status'] == 'open' \ and order['type'] == 'limit' \ and order['side'] == 'buy': - exchange.cancel_order(trade.open_order_id, trade.pair) + self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair) trade.close(order.get('price') or trade.open_rate) # Do the best effort, if we don't know 'filled' amount, don't try selling if order['filled'] is None: @@ -338,7 +337,7 @@ class RPC(object): return # Get current rate and execute sell - current_rate = exchange.get_ticker(trade.pair, False)['bid'] + current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] self._freqtrade.execute_sell(trade, current_rate) # ---- EOF def _exec_forcesell ---- diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index e69de29bb..e1dc7bb3f 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -0,0 +1,32 @@ +import logging +from copy import deepcopy + +from freqtrade.strategy.interface import IStrategy + + +logger = logging.getLogger(__name__) + + +def import_strategy(strategy: IStrategy) -> IStrategy: + """ + Imports given Strategy instance to global scope + of freqtrade.strategy and returns an instance of it + """ + # Copy all attributes from base class and class + attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__}) + # Adjust module name + attr['__module__'] = 'freqtrade.strategy' + + name = strategy.__class__.__name__ + clazz = type(name, (IStrategy,), attr) + + logger.debug( + 'Imported strategy %s.%s as %s.%s', + strategy.__module__, strategy.__class__.__name__, + clazz.__module__, strategy.__class__.__name__, + ) + + # Modify global scope to declare class + globals()[name] = clazz + + return clazz() diff --git a/freqtrade/strategy/resolver.py b/freqtrade/strategy/resolver.py index 3fd39bca3..3c7836291 100644 --- a/freqtrade/strategy/resolver.py +++ b/freqtrade/strategy/resolver.py @@ -11,6 +11,7 @@ from collections import OrderedDict from typing import Optional, Dict, Type from freqtrade import constants +from freqtrade.strategy import import_strategy from freqtrade.strategy.interface import IStrategy @@ -83,7 +84,7 @@ class StrategyResolver(object): strategy = self._search_strategy(path, strategy_name) if strategy: logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) - return strategy + return import_strategy(strategy) raise ImportError( "Impossible to load Strategy '{}'. This class does not exist" @@ -100,7 +101,7 @@ class StrategyResolver(object): """ # Generate spec based on absolute path - spec = importlib.util.spec_from_file_location('user_data.strategies', module_path) + spec = importlib.util.spec_from_file_location('unknown', module_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # type: ignore # importlib does not use typehints diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 5eed0a08f..d9ccaa325 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -13,6 +13,7 @@ from telegram import Chat, Message, Update from freqtrade.analyze import Analyze from freqtrade import constants +from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot logging.getLogger('').setLevel(logging.INFO) @@ -26,6 +27,20 @@ def log_has(line, logs): False) +def patch_exchange(mocker, api_mock=None) -> None: + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + if api_mock: + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + else: + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) + + +def get_patched_exchange(mocker, config, api_mock=None) -> Exchange: + patch_exchange(mocker, api_mock) + exchange = Exchange(config) + return exchange + + # Functions for recurrent object patching def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: """ @@ -39,7 +54,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) + patch_exchange(mocker, None) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock()) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 97a723929..53e59b34b 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -3,33 +3,20 @@ import logging from copy import deepcopy from random import randint +from datetime import datetime from unittest.mock import MagicMock, PropertyMock import ccxt import pytest -import freqtrade.exchange as exchange from freqtrade import OperationalException, DependencyException, TemporaryError -from freqtrade.exchange import (init, validate_pairs, buy, sell, get_balance, get_balances, - get_ticker, get_ticker_history, cancel_order, get_name, get_fee, - get_id, get_pair_detail_url, get_amount_lots) -from freqtrade.tests.conftest import log_has - -API_INIT = False - - -def maybe_init_api(conf, mocker, force=False): - global API_INIT - if force or not API_INIT: - mocker.patch('freqtrade.exchange.validate_pairs', - side_effect=lambda s: True) - init(config=conf) - API_INIT = True +from freqtrade.exchange import Exchange, API_RETRY_COUNT +from freqtrade.tests.conftest import log_has, get_patched_exchange def test_init(default_conf, mocker, caplog): caplog.set_level(logging.INFO) - maybe_init_api(default_conf, mocker, True) + get_patched_exchange(mocker, default_conf) assert log_has('Instance is running with dry_run enabled', caplog.record_tuples) @@ -39,7 +26,7 @@ def test_init_exception(default_conf): with pytest.raises( OperationalException, match='Exchange {} is not supported'.format(default_conf['exchange']['name'])): - init(config=default_conf) + Exchange(default_conf) def test_validate_pairs(default_conf, mocker): @@ -50,18 +37,17 @@ def test_validate_pairs(default_conf, mocker): id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock - mocker.patch('freqtrade.exchange._API', api_mock) - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - validate_pairs(default_conf['exchange']['pair_whitelist']) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + Exchange(default_conf) def test_validate_pairs_not_available(default_conf, mocker): api_mock = MagicMock() api_mock.load_markets = MagicMock(return_value={}) - mocker.patch('freqtrade.exchange._API', api_mock) - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + with pytest.raises(OperationalException, match=r'not available'): - validate_pairs(default_conf['exchange']['pair_whitelist']) + Exchange(default_conf) def test_validate_pairs_not_compatible(default_conf, mocker): @@ -71,25 +57,27 @@ def test_validate_pairs_not_compatible(default_conf, mocker): }) conf = deepcopy(default_conf) conf['stake_currency'] = 'ETH' - mocker.patch('freqtrade.exchange._API', api_mock) - mocker.patch.dict('freqtrade.exchange._CONF', conf) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + with pytest.raises(OperationalException, match=r'not compatible'): - validate_pairs(conf['exchange']['pair_whitelist']) + Exchange(conf) def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() - api_mock.name = 'Binance' - mocker.patch('freqtrade.exchange._API', api_mock) - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) api_mock.load_markets = MagicMock(return_value={}) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) + with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'): - validate_pairs(default_conf['exchange']['pair_whitelist']) + Exchange(default_conf) api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError()) - validate_pairs(default_conf['exchange']['pair_whitelist']) + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + Exchange(default_conf) assert log_has('Unable to validate pairs (assuming they are correct). Reason: ', caplog.record_tuples) @@ -99,22 +87,21 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog): conf = deepcopy(default_conf) conf['stake_currency'] = 'ETH' api_mock = MagicMock() - api_mock.name = 'binance' - mocker.patch('freqtrade.exchange._API', api_mock) - mocker.patch.dict('freqtrade.exchange._CONF', conf) + api_mock.name = MagicMock(return_value='binance') + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) with pytest.raises( OperationalException, match=r'Pair ETH/BTC not compatible with stake_currency: ETH' ): - validate_pairs(default_conf['exchange']['pair_whitelist']) + Exchange(conf) def test_buy_dry_run(default_conf, mocker): default_conf['dry_run'] = True - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + exchange = get_patched_exchange(mocker, default_conf) - order = buy(pair='ETH/BTC', rate=200, amount=1) + order = exchange.buy(pair='ETH/BTC', rate=200, amount=1) assert 'id' in order assert 'dry_run_buy_' in order['id'] @@ -128,12 +115,10 @@ def test_buy_prod(default_conf, mocker): 'foo': 'bar' } }) - mocker.patch('freqtrade.exchange._API', api_mock) - default_conf['dry_run'] = False - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + exchange = get_patched_exchange(mocker, default_conf, api_mock) - order = buy(pair='ETH/BTC', rate=200, amount=1) + order = exchange.buy(pair='ETH/BTC', rate=200, amount=1) assert 'id' in order assert 'info' in order assert order['id'] == order_id @@ -141,30 +126,30 @@ def test_buy_prod(default_conf, mocker): # test exception handling with pytest.raises(DependencyException): api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InsufficientFunds) - mocker.patch('freqtrade.exchange._API', api_mock) - buy(pair='ETH/BTC', rate=200, amount=1) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.buy(pair='ETH/BTC', rate=200, amount=1) with pytest.raises(DependencyException): api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InvalidOrder) - mocker.patch('freqtrade.exchange._API', api_mock) - buy(pair='ETH/BTC', rate=200, amount=1) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.buy(pair='ETH/BTC', rate=200, amount=1) with pytest.raises(TemporaryError): api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.NetworkError) - mocker.patch('freqtrade.exchange._API', api_mock) - buy(pair='ETH/BTC', rate=200, amount=1) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.buy(pair='ETH/BTC', rate=200, amount=1) with pytest.raises(OperationalException): api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.BaseError) - mocker.patch('freqtrade.exchange._API', api_mock) - buy(pair='ETH/BTC', rate=200, amount=1) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.buy(pair='ETH/BTC', rate=200, amount=1) def test_sell_dry_run(default_conf, mocker): default_conf['dry_run'] = True - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) + exchange = get_patched_exchange(mocker, default_conf) - order = sell(pair='ETH/BTC', rate=200, amount=1) + order = exchange.sell(pair='ETH/BTC', rate=200, amount=1) assert 'id' in order assert 'dry_run_sell_' in order['id'] @@ -178,12 +163,11 @@ def test_sell_prod(default_conf, mocker): 'foo': 'bar' } }) - mocker.patch('freqtrade.exchange._API', api_mock) - default_conf['dry_run'] = False - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - order = sell(pair='ETH/BTC', rate=200, amount=1) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + order = exchange.sell(pair='ETH/BTC', rate=200, amount=1) assert 'id' in order assert 'info' in order assert order['id'] == order_id @@ -191,53 +175,52 @@ def test_sell_prod(default_conf, mocker): # test exception handling with pytest.raises(DependencyException): api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InsufficientFunds) - mocker.patch('freqtrade.exchange._API', api_mock) - sell(pair='ETH/BTC', rate=200, amount=1) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.sell(pair='ETH/BTC', rate=200, amount=1) with pytest.raises(DependencyException): api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InvalidOrder) - mocker.patch('freqtrade.exchange._API', api_mock) - sell(pair='ETH/BTC', rate=200, amount=1) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.sell(pair='ETH/BTC', rate=200, amount=1) with pytest.raises(TemporaryError): api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.NetworkError) - mocker.patch('freqtrade.exchange._API', api_mock) - sell(pair='ETH/BTC', rate=200, amount=1) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.sell(pair='ETH/BTC', rate=200, amount=1) with pytest.raises(OperationalException): api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.BaseError) - mocker.patch('freqtrade.exchange._API', api_mock) - sell(pair='ETH/BTC', rate=200, amount=1) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.sell(pair='ETH/BTC', rate=200, amount=1) def test_get_balance_dry_run(default_conf, mocker): default_conf['dry_run'] = True - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - assert get_balance(currency='BTC') == 999.9 + exchange = get_patched_exchange(mocker, default_conf) + assert exchange.get_balance(currency='BTC') == 999.9 def test_get_balance_prod(default_conf, mocker): api_mock = MagicMock() api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4}}) - mocker.patch('freqtrade.exchange._API', api_mock) - default_conf['dry_run'] = False - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - assert get_balance(currency='BTC') == 123.4 + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + assert exchange.get_balance(currency='BTC') == 123.4 with pytest.raises(OperationalException): api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) - mocker.patch('freqtrade.exchange._API', api_mock) - get_balance(currency='BTC') + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + exchange.get_balance(currency='BTC') def test_get_balances_dry_run(default_conf, mocker): default_conf['dry_run'] = True - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - - assert get_balances() == {} + exchange = get_patched_exchange(mocker, default_conf) + assert exchange.get_balances() == {} def test_get_balances_prod(default_conf, mocker): @@ -253,33 +236,73 @@ def test_get_balances_prod(default_conf, mocker): '2ST': balance_item, '3ST': balance_item }) - mocker.patch('freqtrade.exchange._API', api_mock) - default_conf['dry_run'] = False - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - - assert len(get_balances()) == 3 - assert get_balances()['1ST']['free'] == 10.0 - assert get_balances()['1ST']['total'] == 10.0 - assert get_balances()['1ST']['used'] == 0.0 + exchange = get_patched_exchange(mocker, default_conf, api_mock) + 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 with pytest.raises(TemporaryError): api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError) - mocker.patch('freqtrade.exchange._API', api_mock) - get_balances() - assert api_mock.fetch_balance.call_count == exchange.API_RETRY_COUNT + 1 + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_balances() + assert api_mock.fetch_balance.call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) - mocker.patch('freqtrade.exchange._API', api_mock) - get_balances() + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_balances() assert api_mock.fetch_balance.call_count == 1 -# This test is somewhat redundant with -# test_exchange_bittrex.py::test_exchange_bittrex_get_ticker +def test_get_tickers(default_conf, mocker): + api_mock = MagicMock() + tick = {'ETH/BTC': { + 'symbol': 'ETH/BTC', + 'bid': 0.5, + 'ask': 1, + 'last': 42, + }, 'BCH/BTC': { + 'symbol': 'BCH/BTC', + 'bid': 0.6, + 'ask': 0.5, + 'last': 41, + } + } + api_mock.fetch_tickers = MagicMock(return_value=tick) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + # retrieve original ticker + tickers = exchange.get_tickers() + + assert 'ETH/BTC' in tickers + assert 'BCH/BTC' in tickers + assert tickers['ETH/BTC']['bid'] == 0.5 + assert tickers['ETH/BTC']['ask'] == 1 + assert tickers['BCH/BTC']['bid'] == 0.6 + assert tickers['BCH/BTC']['ask'] == 0.5 + + with pytest.raises(TemporaryError): # test retrier + api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NetworkError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_tickers() + + with pytest.raises(OperationalException): + api_mock.fetch_tickers = MagicMock(side_effect=ccxt.BaseError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_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_tickers() + + api_mock.fetch_tickers = MagicMock(return_value={}) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_tickers() + + def test_get_ticker(default_conf, mocker): - maybe_init_api(default_conf, mocker) api_mock = MagicMock() tick = { 'symbol': 'ETH/BTC', @@ -288,10 +311,9 @@ def test_get_ticker(default_conf, mocker): 'last': 0.0001, } api_mock.fetch_ticker = MagicMock(return_value=tick) - mocker.patch('freqtrade.exchange._API', api_mock) - + exchange = get_patched_exchange(mocker, default_conf, api_mock) # retrieve original ticker - ticker = get_ticker(pair='ETH/BTC') + ticker = exchange.get_ticker(pair='ETH/BTC') assert ticker['bid'] == 0.00001098 assert ticker['ask'] == 0.00001099 @@ -304,38 +326,38 @@ def test_get_ticker(default_conf, mocker): 'last': 42, } api_mock.fetch_ticker = MagicMock(return_value=tick) - mocker.patch('freqtrade.exchange._API', api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock) # if not caching the result we should get the same ticker # if not fetching a new result we should get the cached ticker - ticker = get_ticker(pair='ETH/BTC') + ticker = exchange.get_ticker(pair='ETH/BTC') assert api_mock.fetch_ticker.call_count == 1 assert ticker['bid'] == 0.5 assert ticker['ask'] == 1 - assert 'ETH/BTC' in exchange._CACHED_TICKER - assert exchange._CACHED_TICKER['ETH/BTC']['bid'] == 0.5 - assert exchange._CACHED_TICKER['ETH/BTC']['ask'] == 1 + assert 'ETH/BTC' in exchange._cached_ticker + assert exchange._cached_ticker['ETH/BTC']['bid'] == 0.5 + assert exchange._cached_ticker['ETH/BTC']['ask'] == 1 # Test caching api_mock.fetch_ticker = MagicMock() - get_ticker(pair='ETH/BTC', refresh=False) + exchange.get_ticker(pair='ETH/BTC', refresh=False) assert api_mock.fetch_ticker.call_count == 0 with pytest.raises(TemporaryError): # test retrier api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError) - mocker.patch('freqtrade.exchange._API', api_mock) - get_ticker(pair='ETH/BTC', refresh=True) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_ticker(pair='ETH/BTC', refresh=True) with pytest.raises(OperationalException): api_mock.fetch_ticker = MagicMock(side_effect=ccxt.BaseError) - mocker.patch('freqtrade.exchange._API', api_mock) - get_ticker(pair='ETH/BTC', refresh=True) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_ticker(pair='ETH/BTC', refresh=True) api_mock.fetch_ticker = MagicMock(return_value={}) - mocker.patch('freqtrade.exchange._API', api_mock) - get_ticker(pair='ETH/BTC', refresh=True) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_ticker(pair='ETH/BTC', refresh=True) def make_fetch_ohlcv_mock(data): @@ -361,10 +383,10 @@ def test_get_ticker_history(default_conf, mocker): ] type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) - mocker.patch('freqtrade.exchange._API', api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock) # retrieve original ticker - ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval']) + ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) assert ticks[0][0] == 1511686200000 assert ticks[0][1] == 1 assert ticks[0][2] == 2 @@ -384,9 +406,9 @@ def test_get_ticker_history(default_conf, mocker): ] ] api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick)) - mocker.patch('freqtrade.exchange._API', api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock) - ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval']) + ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) assert ticks[0][0] == 1511686210000 assert ticks[0][1] == 6 assert ticks[0][2] == 7 @@ -396,15 +418,15 @@ def test_get_ticker_history(default_conf, mocker): with pytest.raises(TemporaryError): # test retrier api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError) - mocker.patch('freqtrade.exchange._API', api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock) # new symbol to get around cache - get_ticker_history('ABCD/BTC', default_conf['ticker_interval']) + exchange.get_ticker_history('ABCD/BTC', default_conf['ticker_interval']) with pytest.raises(OperationalException): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError) - mocker.patch('freqtrade.exchange._API', api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock) # new symbol to get around cache - get_ticker_history('EFGH/BTC', default_conf['ticker_interval']) + exchange.get_ticker_history('EFGH/BTC', default_conf['ticker_interval']) def test_get_ticker_history_sort(default_conf, mocker): @@ -426,10 +448,11 @@ def test_get_ticker_history_sort(default_conf, mocker): ] type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) - mocker.patch('freqtrade.exchange._API', api_mock) + + exchange = get_patched_exchange(mocker, default_conf, api_mock) # Test the ticker history sort - ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval']) + ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) assert ticks[0][0] == 1527830400000 assert ticks[0][1] == 0.07649 assert ticks[0][2] == 0.07651 @@ -460,10 +483,9 @@ def test_get_ticker_history_sort(default_conf, mocker): ] type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) - mocker.patch('freqtrade.exchange._API', api_mock) - + exchange = get_patched_exchange(mocker, default_conf, api_mock) # Test the ticker history sort - ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval']) + ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) assert ticks[0][0] == 1527827700000 assert ticks[0][1] == 0.07659999 assert ticks[0][2] == 0.0766 @@ -481,117 +503,194 @@ def test_get_ticker_history_sort(default_conf, mocker): def test_cancel_order_dry_run(default_conf, mocker): default_conf['dry_run'] = True - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - - assert cancel_order(order_id='123', pair='TKN/BTC') is None + exchange = get_patched_exchange(mocker, default_conf) + 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): default_conf['dry_run'] = False - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) api_mock = MagicMock() api_mock.cancel_order = MagicMock(return_value=123) - mocker.patch('freqtrade.exchange._API', api_mock) - assert cancel_order(order_id='_', pair='TKN/BTC') == 123 + exchange = get_patched_exchange(mocker, default_conf, api_mock) + assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 with pytest.raises(TemporaryError): api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError) - mocker.patch('freqtrade.exchange._API', api_mock) - cancel_order(order_id='_', pair='TKN/BTC') - assert api_mock.cancel_order.call_count == exchange.API_RETRY_COUNT + 1 + + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.cancel_order(order_id='_', pair='TKN/BTC') + assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1 with pytest.raises(DependencyException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) - mocker.patch('freqtrade.exchange._API', api_mock) - cancel_order(order_id='_', pair='TKN/BTC') - assert api_mock.cancel_order.call_count == exchange.API_RETRY_COUNT + 1 + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.cancel_order(order_id='_', pair='TKN/BTC') + assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError) - mocker.patch('freqtrade.exchange._API', api_mock) - cancel_order(order_id='_', pair='TKN/BTC') + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.cancel_order(order_id='_', pair='TKN/BTC') assert api_mock.cancel_order.call_count == 1 def test_get_order(default_conf, mocker): default_conf['dry_run'] = True - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) order = MagicMock() order.myid = 123 - exchange._DRY_RUN_OPEN_ORDERS['X'] = order + exchange = get_patched_exchange(mocker, default_conf) + exchange._dry_run_open_orders['X'] = order print(exchange.get_order('X', 'TKN/BTC')) assert exchange.get_order('X', 'TKN/BTC').myid == 123 default_conf['dry_run'] = False - mocker.patch.dict('freqtrade.exchange._CONF', default_conf) api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) - mocker.patch('freqtrade.exchange._API', api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock) assert exchange.get_order('X', 'TKN/BTC') == 456 with pytest.raises(TemporaryError): api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError) - mocker.patch('freqtrade.exchange._API', api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.get_order(order_id='_', pair='TKN/BTC') - assert api_mock.fetch_order.call_count == exchange.API_RETRY_COUNT + 1 + assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 with pytest.raises(DependencyException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) - mocker.patch('freqtrade.exchange._API', api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.get_order(order_id='_', pair='TKN/BTC') - assert api_mock.fetch_order.call_count == exchange.API_RETRY_COUNT + 1 + assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 with pytest.raises(OperationalException): api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError) - mocker.patch('freqtrade.exchange._API', api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.get_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 -def test_get_name(default_conf, mocker): - mocker.patch('freqtrade.exchange.validate_pairs', +def test_name(default_conf, mocker): + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', side_effect=lambda s: True) default_conf['exchange']['name'] = 'binance' - init(default_conf) + exchange = Exchange(default_conf) - assert get_name() == 'Binance' + assert exchange.name == 'Binance' -def test_get_id(default_conf, mocker): - mocker.patch('freqtrade.exchange.validate_pairs', +def test_id(default_conf, mocker): + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', side_effect=lambda s: True) default_conf['exchange']['name'] = 'binance' - init(default_conf) - - assert get_id() == 'binance' + exchange = Exchange(default_conf) + assert exchange.id == 'binance' -def test_get_pair_detail_url(default_conf, mocker): - mocker.patch('freqtrade.exchange.validate_pairs', +def test_get_pair_detail_url(default_conf, mocker, caplog): + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', side_effect=lambda s: True) default_conf['exchange']['name'] = 'binance' - init(default_conf) + exchange = Exchange(default_conf) - url = get_pair_detail_url('TKN/ETH') + url = exchange.get_pair_detail_url('TKN/ETH') assert 'TKN' in url assert 'ETH' in url - url = get_pair_detail_url('LOOONG/BTC') + url = exchange.get_pair_detail_url('LOOONG/BTC') assert 'LOOONG' in url assert 'BTC' in url default_conf['exchange']['name'] = 'bittrex' - init(default_conf) + exchange = Exchange(default_conf) - url = get_pair_detail_url('TKN/ETH') + url = exchange.get_pair_detail_url('TKN/ETH') assert 'TKN' in url assert 'ETH' in url - url = get_pair_detail_url('LOOONG/BTC') + 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) + + +def test_get_trades_for_order(default_conf, mocker): + order_id = 'ABCD-ABCD' + since = datetime(2018, 5, 5) + default_conf["dry_run"] = False + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + api_mock = MagicMock() + + api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV', + 'order': 'ABCD-ABCD', + 'info': {'pair': 'XLTCZBTC', + 'time': 1519860024.4388, + 'type': 'buy', + 'ordertype': 'limit', + 'price': '20.00000', + 'cost': '38.62000', + 'fee': '0.06179', + 'vol': '5', + 'id': 'ABCD-ABCD'}, + 'timestamp': 1519860024438, + 'datetime': '2018-02-28T23:20:24.438Z', + 'symbol': 'LTC/BTC', + 'type': 'limit', + 'side': 'buy', + 'price': 165.0, + 'amount': 0.2340606, + 'fee': {'cost': 0.06179, 'currency': 'BTC'} + }]) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since) + assert len(orders) == 1 + assert orders[0]['price'] == 165 + + # test Exceptions + with pytest.raises(OperationalException): + api_mock = MagicMock() + api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.BaseError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_trades_for_order(order_id, 'LTC/BTC', since) + + with pytest.raises(TemporaryError): + api_mock = MagicMock() + api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.NetworkError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_trades_for_order(order_id, 'LTC/BTC', since) + assert api_mock.fetch_my_trades.call_count == API_RETRY_COUNT + 1 + + +def test_get_markets(default_conf, mocker, markets): + api_mock = MagicMock() + api_mock.fetch_markets = markets + exchange = get_patched_exchange(mocker, default_conf, api_mock) + ret = exchange.get_markets() + assert isinstance(ret, list) + assert len(ret) == 6 + + assert ret[0]["id"] == "ethbtc" + assert ret[0]["symbol"] == "ETH/BTC" + + # test Exceptions + with pytest.raises(OperationalException): + api_mock = MagicMock() + api_mock.fetch_markets = MagicMock(side_effect=ccxt.BaseError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_markets() + + with pytest.raises(TemporaryError): + api_mock = MagicMock() + api_mock.fetch_markets = MagicMock(side_effect=ccxt.NetworkError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_markets() + assert api_mock.fetch_markets.call_count == API_RETRY_COUNT + 1 + def test_get_fee(default_conf, mocker): api_mock = MagicMock() @@ -601,12 +700,32 @@ def test_get_fee(default_conf, mocker): 'rate': 0.025, 'cost': 0.05 }) - mocker.patch('freqtrade.exchange._API', api_mock) - assert get_fee() == 0.025 + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + assert exchange.get_fee() == 0.025 + + # test Exceptions + with pytest.raises(OperationalException): + api_mock = MagicMock() + api_mock.calculate_fee = MagicMock(side_effect=ccxt.BaseError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_fee() + + with pytest.raises(TemporaryError): + api_mock = MagicMock() + api_mock.calculate_fee = MagicMock(side_effect=ccxt.NetworkError) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.get_fee() + assert api_mock.calculate_fee.call_count == API_RETRY_COUNT + 1 def test_get_amount_lots(default_conf, mocker): api_mock = MagicMock() api_mock.amount_to_lots = MagicMock(return_value=1.0) - mocker.patch('freqtrade.exchange._API', api_mock) - assert get_amount_lots('LTC/BTC', 1.54) == 1 + api_mock.markets = None + marketmock = MagicMock() + api_mock.load_markets = marketmock + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + assert exchange.get_amount_lots('LTC/BTC', 1.54) == 1 + assert marketmock.call_count == 1 diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index e66170a0f..c3d2ad572 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -16,7 +16,7 @@ from freqtrade import optimize, constants, DependencyException from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments, TimeRange from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration -from freqtrade.tests.conftest import log_has +from freqtrade.tests.conftest import log_has, patch_exchange def get_args(args) -> List[str]: @@ -84,7 +84,7 @@ def load_data_test(what): def simple_backtest(config, contour, num_results, mocker) -> None: - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + patch_exchange(mocker) backtesting = Backtesting(config) data = load_data_test(contour) @@ -102,7 +102,8 @@ def simple_backtest(config, contour, num_results, mocker) -> None: assert len(results) == num_results -def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, timerange=None): +def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, + timerange=None, exchange=None): tickerdata = optimize.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) pairdata = {'UNITTEST/BTC': tickerdata} return pairdata @@ -119,7 +120,7 @@ def _load_pair_as_ticks(pair, tickfreq): def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None): data = optimize.load_data(None, ticker_interval='8m', pairs=[pair]) data = trim_dictlist(data, -201) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + patch_exchange(mocker) backtesting = Backtesting(conf) return { 'stake_amount': conf['stake_amount'], @@ -295,8 +296,8 @@ def test_start(mocker, fee, default_conf, caplog) -> None: Test start() function """ start_mock = MagicMock() - mocker.patch('freqtrade.exchange.get_fee', fee) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) @@ -319,7 +320,8 @@ def test_backtesting_init(mocker, default_conf) -> None: """ Test Backtesting._init() method """ - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + patch_exchange(mocker) + get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) assert backtesting.config == default_conf assert isinstance(backtesting.analyze, Analyze) @@ -327,13 +329,15 @@ def test_backtesting_init(mocker, default_conf) -> None: assert callable(backtesting.tickerdata_to_dataframe) assert callable(backtesting.populate_buy_trend) assert callable(backtesting.populate_sell_trend) + get_fee.assert_called() + assert backtesting.fee == 0.5 def test_tickerdata_to_dataframe(default_conf, mocker) -> None: """ Test Backtesting.tickerdata_to_dataframe() method """ - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + patch_exchange(mocker) timerange = TimeRange(None, 'line', 0, -100) tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) tickerlist = {'UNITTEST/BTC': tick} @@ -352,7 +356,7 @@ def test_get_timeframe(default_conf, mocker) -> None: """ Test Backtesting.get_timeframe() method """ - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + patch_exchange(mocker) backtesting = Backtesting(default_conf) data = backtesting.tickerdata_to_dataframe( @@ -371,7 +375,7 @@ def test_generate_text_table(default_conf, mocker): """ Test Backtesting.generate_text_table() method """ - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + patch_exchange(mocker) backtesting = Backtesting(default_conf) results = pd.DataFrame( @@ -408,8 +412,8 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock()) mocker.patch('freqtrade.optimize.load_data', mocked_load_data) - mocker.patch('freqtrade.exchange.get_ticker_history') - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history') + patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.optimize.backtesting.Backtesting', backtest=MagicMock(), @@ -449,8 +453,8 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock()) mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.get_ticker_history') - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history') + patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.optimize.backtesting.Backtesting', backtest=MagicMock(), @@ -477,8 +481,8 @@ def test_backtest(default_conf, fee, mocker) -> None: """ Test Backtesting.backtest() method """ - mocker.patch('freqtrade.exchange.get_fee', fee) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + patch_exchange(mocker) backtesting = Backtesting(default_conf) data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC']) @@ -499,8 +503,8 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: """ Test Backtesting.backtest() method with 1 min ticker """ - mocker.patch('freqtrade.exchange.get_fee', fee) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + patch_exchange(mocker) backtesting = Backtesting(default_conf) # Run a backtesting for an exiting 5min ticker_interval @@ -522,7 +526,7 @@ def test_processed(default_conf, mocker) -> None: """ Test Backtesting.backtest() method with offline data """ - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + patch_exchange(mocker) backtesting = Backtesting(default_conf) dict_of_tickerrows = load_data_test('raise') @@ -536,7 +540,7 @@ def test_processed(default_conf, mocker) -> None: def test_backtest_pricecontours(default_conf, fee, mocker) -> None: - mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) tests = [['raise', 18], ['lower', 0], ['sine', 16]] for [contour, numres] in tests: simple_backtest(default_conf, contour, numres, mocker) @@ -544,8 +548,8 @@ def test_backtest_pricecontours(default_conf, fee, mocker) -> None: # Test backtest using offline data (testdata directory) def test_backtest_ticks(default_conf, fee, mocker): - mocker.patch('freqtrade.exchange.get_fee', fee) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + patch_exchange(mocker) ticks = [1, 5] fun = Backtesting(default_conf).populate_buy_trend for _ in ticks: @@ -564,7 +568,6 @@ def test_backtest_clash_buy_sell(mocker, default_conf): sell_value = 1 return _trend(dataframe, buy_value, sell_value) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtesting = Backtesting(default_conf) backtesting.populate_buy_trend = fun # Override @@ -580,7 +583,6 @@ def test_backtest_only_sell(mocker, default_conf): sell_value = 1 return _trend(dataframe, buy_value, sell_value) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtesting = Backtesting(default_conf) backtesting.populate_buy_trend = fun # Override @@ -590,8 +592,7 @@ def test_backtest_only_sell(mocker, default_conf): def test_backtest_alternate_buy_sell(default_conf, fee, mocker): - mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC') backtesting = Backtesting(default_conf) backtesting.populate_buy_trend = _trend_alternate # Override @@ -606,8 +607,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker): def test_backtest_record(default_conf, fee, mocker): names = [] records = [] - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) - mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee) + patch_exchange(mocker) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch( 'freqtrade.optimize.backtesting.file_dump_json', new=lambda n, r: (names.append(n), records.append(r)) @@ -655,9 +656,9 @@ def test_backtest_record(default_conf, fee, mocker): def test_backtest_start_live(default_conf, mocker, caplog): conf = deepcopy(default_conf) conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] - mocker.patch('freqtrade.exchange.get_ticker_history', - new=lambda n, i: _load_pair_as_ticks(n, i)) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', + new=lambda s, n, i: _load_pair_as_ticks(n, i)) + patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) mocker.patch('freqtrade.configuration.open', mocker.mock_open( diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index f8dce5fd6..8ad1932af 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -10,7 +10,7 @@ import pytest from freqtrade.optimize.__init__ import load_tickerdata_file from freqtrade.optimize.hyperopt import Hyperopt, start from freqtrade.strategy.resolver import StrategyResolver -from freqtrade.tests.conftest import log_has +from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.optimize.test_backtesting import get_args # Avoid to reinit the same object again and again @@ -22,8 +22,7 @@ _HYPEROPT = None def init_hyperopt(default_conf, mocker): global _HYPEROPT_INITIALIZED, _HYPEROPT if not _HYPEROPT_INITIALIZED: - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock()) + patch_exchange(mocker) _HYPEROPT = Hyperopt(default_conf) _HYPEROPT_INITIALIZED = True @@ -61,8 +60,12 @@ def test_start(mocker, default_conf, caplog) -> None: Test start() function """ start_mock = MagicMock() + mocker.patch( + 'freqtrade.configuration.Configuration._load_config_file', + lambda *args, **kwargs: default_conf + ) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) - mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) + patch_exchange(mocker) args = [ '--config', 'config.json', @@ -178,7 +181,7 @@ def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None: mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result) - mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) + patch_exchange(mocker) StrategyResolver({'strategy': 'DefaultStrategy'}) hyperopt = Hyperopt(conf) @@ -222,7 +225,7 @@ def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) -> conf.update({'epochs': 1}) conf.update({'timerange': None}) conf.update({'spaces': 'all'}) - mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) + patch_exchange(mocker) StrategyResolver({'strategy': 'DefaultStrategy'}) hyperopt = Hyperopt(conf) @@ -263,7 +266,7 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, defa mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock()) + patch_exchange(mocker) StrategyResolver({'strategy': 'DefaultStrategy'}) hyperopt = Hyperopt(conf) @@ -334,7 +337,7 @@ def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None: trials = create_trials(mocker) mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock()) + patch_exchange(mocker) mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) conf = deepcopy(default_conf) @@ -499,7 +502,7 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None: 'freqtrade.optimize.hyperopt.Hyperopt.backtest', MagicMock(return_value=backtest_result) ) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock()) + patch_exchange(mocker) optimizer_param = { 'adx': {'enabled': False}, diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index bac8a6b36..624f8ed77 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -12,7 +12,7 @@ from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \ download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \ load_cached_data_for_updating from freqtrade.arguments import TimeRange -from freqtrade.tests.conftest import log_has +from freqtrade.tests.conftest import log_has, get_patched_exchange # Change this if modifying UNITTEST/BTC testdatafile _BTC_UNITTEST_LENGTH = 13681 @@ -49,12 +49,11 @@ def _clean_test_file(file: str) -> None: os.rename(file_swp, file) -def test_load_data_30min_ticker(ticker_history, mocker, caplog) -> None: +def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None: """ Test load_data() with 30 min ticker """ - mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history) - + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json') _backup_file(file, copy_file=True) optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m') @@ -63,11 +62,11 @@ def test_load_data_30min_ticker(ticker_history, mocker, caplog) -> None: _clean_test_file(file) -def test_load_data_5min_ticker(ticker_history, mocker, caplog) -> None: +def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None: """ Test load_data() with 5 min ticker """ - mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json') _backup_file(file, copy_file=True) @@ -81,7 +80,7 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: """ Test load_data() with 1 min ticker """ - mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history) + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json') _backup_file(file, copy_file=True) @@ -91,12 +90,12 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: _clean_test_file(file) -def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None: +def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_conf) -> None: """ Test load_data() with 1 min ticker """ - mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history) - + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) + exchange = get_patched_exchange(mocker, default_conf) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') _backup_file(file) @@ -114,6 +113,7 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None: optimize.load_data(None, ticker_interval='1m', refresh_pairs=True, + exchange=exchange, pairs=['MEME/BTC']) assert os.path.isfile(file) is True assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples) @@ -124,9 +124,9 @@ def test_testdata_path() -> None: assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None) -def test_download_pairs(ticker_history, mocker) -> None: - mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history) - +def test_download_pairs(ticker_history, mocker, default_conf) -> None: + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) + exchange = get_patched_exchange(mocker, default_conf) file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json') file2_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-1m.json') @@ -140,7 +140,8 @@ def test_download_pairs(ticker_history, mocker) -> None: assert os.path.isfile(file1_1) is False assert os.path.isfile(file2_1) is False - assert download_pairs(None, pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='1m') is True + assert download_pairs(None, exchange, + pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='1m') is True assert os.path.isfile(file1_1) is True assert os.path.isfile(file2_1) is True @@ -152,7 +153,8 @@ def test_download_pairs(ticker_history, mocker) -> None: assert os.path.isfile(file1_5) is False assert os.path.isfile(file2_5) is False - assert download_pairs(None, pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='5m') is True + assert download_pairs(None, exchange, + pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='5m') is True assert os.path.isfile(file1_5) is True assert os.path.isfile(file2_5) is True @@ -265,30 +267,32 @@ def test_load_cached_data_for_updating(mocker) -> None: assert start_ts is None -def test_download_pairs_exception(ticker_history, mocker, caplog) -> None: - mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history) +def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None: + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata', side_effect=BaseException('File Error')) + exchange = get_patched_exchange(mocker, default_conf) file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json') _backup_file(file1_1) _backup_file(file1_5) - download_pairs(None, pairs=['MEME/BTC'], ticker_interval='1m') + download_pairs(None, exchange, pairs=['MEME/BTC'], ticker_interval='1m') # clean files freshly downloaded _clean_test_file(file1_1) _clean_test_file(file1_5) assert log_has('Failed to download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples) -def test_download_backtesting_testdata(ticker_history, mocker) -> None: - mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history) +def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None: + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) + exchange = get_patched_exchange(mocker, default_conf) # Download a 1 min ticker file file1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'XEL_BTC-1m.json') _backup_file(file1) - download_backtesting_testdata(None, pair="XEL/BTC", tick_interval='1m') + download_backtesting_testdata(None, exchange, pair="XEL/BTC", tick_interval='1m') assert os.path.isfile(file1) is True _clean_test_file(file1) @@ -296,21 +300,21 @@ def test_download_backtesting_testdata(ticker_history, mocker) -> None: file2 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'STORJ_BTC-5m.json') _backup_file(file2) - download_backtesting_testdata(None, pair="STORJ/BTC", tick_interval='5m') + download_backtesting_testdata(None, exchange, pair="STORJ/BTC", tick_interval='5m') assert os.path.isfile(file2) is True _clean_test_file(file2) -def test_download_backtesting_testdata2(mocker) -> None: +def test_download_backtesting_testdata2(mocker, default_conf) -> None: tick = [ [1509836520000, 0.00162008, 0.00162008, 0.00162008, 0.00162008, 108.14853839], [1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199] ] json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) - mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick) - - download_backtesting_testdata(None, pair="UNITTEST/BTC", tick_interval='1m') - download_backtesting_testdata(None, pair="UNITTEST/BTC", tick_interval='3m') + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=tick) + exchange = get_patched_exchange(mocker, default_conf) + download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m') + download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m') assert json_dump_mock.call_count == 2 @@ -326,8 +330,10 @@ def test_load_tickerdata_file() -> None: def test_init(default_conf, mocker) -> None: + exchange = get_patched_exchange(mocker, default_conf) assert {} == optimize.load_data( '', + exchange=exchange, pairs=[], refresh_pairs=True, ticker_interval=default_conf['ticker_interval'] diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index baea0fa67..11db7ffb3 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -33,7 +33,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -80,7 +80,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -114,7 +114,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -170,7 +170,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -194,7 +194,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Update the ticker with a market going up mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker_sell_up ) @@ -209,7 +209,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Update the ticker with a market going up mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker_sell_up ) @@ -247,7 +247,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -267,7 +267,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, trade.update(limit_buy_order) # Update the ticker with a market going up mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker_sell_up, get_fee=fee @@ -319,7 +319,7 @@ def test_rpc_balance_handle(default_conf, mocker): mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_balances=MagicMock(return_value=mock_balance) ) @@ -347,7 +347,7 @@ def test_rpc_start(mocker, default_conf) -> None: patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=MagicMock() ) @@ -373,7 +373,7 @@ def test_rpc_stop(mocker, default_conf) -> None: patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=MagicMock() ) @@ -401,7 +401,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: cancel_order_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, cancel_order=cancel_order_mock, @@ -447,7 +447,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: trade = Trade.query.filter(Trade.id == '1').first() filled_amount = trade.amount / 2 mocker.patch( - 'freqtrade.freqtradebot.exchange.get_order', + 'freqtrade.exchange.Exchange.get_order', return_value={ 'status': 'open', 'type': 'limit', @@ -466,7 +466,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it mocker.patch( - 'freqtrade.freqtradebot.exchange.get_order', + 'freqtrade.exchange.Exchange.get_order', return_value={ 'status': 'open', 'type': 'limit', @@ -482,7 +482,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: freqtradebot.create_trade() # make an limit-sell open trade mocker.patch( - 'freqtrade.freqtradebot.exchange.get_order', + 'freqtrade.exchange.Exchange.get_order', return_value={ 'status': 'open', 'type': 'limit', @@ -503,7 +503,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_balances=MagicMock(return_value=ticker), get_ticker=ticker, @@ -542,7 +542,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_balances=MagicMock(return_value=ticker), get_ticker=ticker, diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index b302ddd46..b2cca9b9a 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -20,7 +20,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc.telegram import Telegram from freqtrade.rpc.telegram import authorized_only from freqtrade.state import State -from freqtrade.tests.conftest import get_patched_freqtradebot, log_has +from freqtrade.tests.conftest import get_patched_freqtradebot, patch_exchange, log_has from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap @@ -100,7 +100,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None: """ patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) + patch_exchange(mocker, None) chat = Chat(0, 0) update = Update(randint(1, 100)) @@ -131,8 +131,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: """ patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) - + patch_exchange(mocker, None) chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) @@ -162,7 +161,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: """ patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) + patch_exchange(mocker) update = Update(randint(1, 100)) update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0)) @@ -198,7 +197,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_pair_detail_url=MagicMock(), @@ -239,7 +238,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -286,7 +285,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), @@ -344,7 +343,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, return_value=15000.0 ) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -414,7 +413,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker ) @@ -454,7 +453,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -489,7 +488,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, msg_mock.reset_mock() # Update the ticker with a market going up - mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', ticker_sell_up) + mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up) trade.update(limit_sell_order) trade.close_date = datetime.utcnow() @@ -554,9 +553,8 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) - mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value=mock_balance) - mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', side_effect=mock_ticker) + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance) + mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) msg_mock = MagicMock() mocker.patch.multiple( @@ -565,7 +563,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) telegram._balance(bot=MagicMock(), update=update) @@ -584,9 +582,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None: Test _balance() method when the Exchange platform returns nothing """ patch_get_signal(mocker, (True, False)) - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) - mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value={}) + mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) msg_mock = MagicMock() mocker.patch.multiple( @@ -595,7 +591,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) telegram._balance(bot=MagicMock(), update=update) @@ -608,17 +604,14 @@ def test_start_handle(default_conf, update, mocker) -> None: """ Test _start() method """ - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) freqtradebot.state = State.STOPPED @@ -632,17 +625,14 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None: """ Test _start() method """ - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) freqtradebot.state = State.RUNNING @@ -658,16 +648,14 @@ def test_stop_handle(default_conf, update, mocker) -> None: Test _stop() method """ patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) freqtradebot.state = State.RUNNING @@ -683,16 +671,14 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: Test _stop() method """ patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) freqtradebot.state = State.STOPPED @@ -706,16 +692,14 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: def test_reload_conf_handle(default_conf, update, mocker) -> None: """ Test _reload_conf() method """ patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) freqtradebot.state = State.RUNNING @@ -737,7 +721,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -754,7 +738,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, assert trade # Increase the price and sell it - mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', ticker_sell_up) + mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up) update.message.text = '/forcesell 1' telegram._forcesell(bot=MagicMock(), update=update) @@ -779,7 +763,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -794,7 +778,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, # Decrease the price and sell it mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker_sell_down ) @@ -823,9 +807,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', 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.get_pair_detail_url', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock()) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -863,7 +847,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) freqtradebot = FreqtradeBot(default_conf) telegram = Telegram(freqtradebot) @@ -906,7 +890,7 @@ def test_performance_handle(default_conf, update, ticker, fee, _send_msg=msg_mock ) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -947,7 +931,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) freqtradebot = FreqtradeBot(default_conf) telegram = Telegram(freqtradebot) @@ -971,13 +955,13 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non _send_msg=msg_mock ) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), get_markets=markets ) - mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) freqtradebot = FreqtradeBot(default_conf) telegram = Telegram(freqtradebot) @@ -1007,14 +991,14 @@ def test_help_handle(default_conf, update, mocker) -> None: Test _help() method """ patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) telegram._help(bot=MagicMock(), update=update) @@ -1027,14 +1011,13 @@ def test_version_handle(default_conf, update, mocker) -> None: Test _version() method """ patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = FreqtradeBot(default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) telegram._version(bot=MagicMock(), update=update) @@ -1047,11 +1030,10 @@ def test_send_msg(default_conf, mocker) -> None: Test send_msg() method """ patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) conf = deepcopy(default_conf) bot = MagicMock() - freqtradebot = FreqtradeBot(conf) + freqtradebot = get_patched_freqtradebot(mocker, conf) telegram = Telegram(freqtradebot) telegram._config['telegram']['enabled'] = True @@ -1064,12 +1046,11 @@ def test_send_msg_network_error(default_conf, mocker, caplog) -> None: Test send_msg() method """ patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) conf = deepcopy(default_conf) bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) - freqtradebot = FreqtradeBot(conf) + freqtradebot = get_patched_freqtradebot(mocker, conf) telegram = Telegram(freqtradebot) telegram._config['telegram']['enabled'] = True diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 244910790..13eac3261 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -1,14 +1,39 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 - import logging import os import pytest +from freqtrade.strategy import import_strategy +from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.resolver import StrategyResolver +def test_import_strategy(caplog): + caplog.set_level(logging.DEBUG) + + strategy = DefaultStrategy() + strategy.some_method = lambda *args, **kwargs: 42 + + assert strategy.__module__ == 'freqtrade.strategy.default_strategy' + assert strategy.some_method() == 42 + + imported_strategy = import_strategy(strategy) + + assert dir(strategy) == dir(imported_strategy) + + assert imported_strategy.__module__ == 'freqtrade.strategy' + assert imported_strategy.some_method() == 42 + + assert ( + 'freqtrade.strategy', + logging.DEBUG, + 'Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy ' + 'as freqtrade.strategy.DefaultStrategy', + ) in caplog.record_tuples + + def test_search_strategy(): default_location = os.path.join(os.path.dirname( os.path.realpath(__file__)), '..', '..', 'strategy' @@ -20,8 +45,7 @@ def test_search_strategy(): def test_load_strategy(result): - resolver = StrategyResolver() - resolver._load_strategy('TestStrategy') + resolver = StrategyResolver({'strategy': 'TestStrategy'}) assert hasattr(resolver.strategy, 'populate_indicators') assert 'adx' in resolver.strategy.populate_indicators(result) diff --git a/freqtrade/tests/test_acl_pair.py b/freqtrade/tests/test_acl_pair.py index 07375260e..608e6a4a1 100644 --- a/freqtrade/tests/test_acl_pair.py +++ b/freqtrade/tests/test_acl_pair.py @@ -32,7 +32,7 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets): freqtradebot = tt.get_patched_freqtradebot(mocker, conf) - mocker.patch('freqtrade.freqtradebot.exchange.get_markets', markets) + mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) refreshedwhitelist = freqtradebot._refresh_whitelist( conf['exchange']['pair_whitelist'] + ['XXX/BTC'] ) @@ -46,7 +46,7 @@ def test_refresh_whitelist(mocker, markets): conf = whitelist_conf() freqtradebot = tt.get_patched_freqtradebot(mocker, conf) - mocker.patch('freqtrade.freqtradebot.exchange.get_markets', markets) + mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist']) # List ordered by BaseVolume @@ -59,7 +59,7 @@ def test_refresh_whitelist_dynamic(mocker, markets, tickers): conf = whitelist_conf() freqtradebot = tt.get_patched_freqtradebot(mocker, conf) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', get_markets=markets, get_tickers=tickers, exchange_has=MagicMock(return_value=True) @@ -78,7 +78,7 @@ def test_refresh_whitelist_dynamic(mocker, markets, tickers): def test_refresh_whitelist_dynamic_empty(mocker, markets_empty): conf = whitelist_conf() freqtradebot = tt.get_patched_freqtradebot(mocker, conf) - mocker.patch('freqtrade.freqtradebot.exchange.get_markets', markets_empty) + mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty) # argument: use the whitelist dynamically by exchange-volume whitelist = [] diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index e8d0816aa..89dac21c6 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -14,7 +14,7 @@ from pandas import DataFrame from freqtrade.analyze import Analyze, SignalType from freqtrade.optimize.__init__ import load_tickerdata_file from freqtrade.arguments import TimeRange -from freqtrade.tests.conftest import log_has +from freqtrade.tests.conftest import log_has, get_patched_exchange # Avoid to reinit the same object again and again _ANALYZE = Analyze({'strategy': 'DefaultStrategy'}) @@ -66,16 +66,16 @@ def test_populates_sell_trend(result): assert 'sell' in dataframe.columns -def test_returns_latest_buy_signal(mocker): - mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock()) - +def test_returns_latest_buy_signal(mocker, default_conf): + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock()) + exchange = get_patched_exchange(mocker, default_conf) mocker.patch.multiple( 'freqtrade.analyze.Analyze', analyze_ticker=MagicMock( return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) ) ) - assert _ANALYZE.get_signal('ETH/BTC', '5m') == (True, False) + assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False) mocker.patch.multiple( 'freqtrade.analyze.Analyze', @@ -83,11 +83,12 @@ def test_returns_latest_buy_signal(mocker): return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) ) ) - assert _ANALYZE.get_signal('ETH/BTC', '5m') == (False, True) + assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True) -def test_returns_latest_sell_signal(mocker): - mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock()) +def test_returns_latest_sell_signal(mocker, default_conf): + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock()) + exchange = get_patched_exchange(mocker, default_conf) mocker.patch.multiple( 'freqtrade.analyze.Analyze', analyze_ticker=MagicMock( @@ -95,7 +96,7 @@ def test_returns_latest_sell_signal(mocker): ) ) - assert _ANALYZE.get_signal('ETH/BTC', '5m') == (False, True) + assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True) mocker.patch.multiple( 'freqtrade.analyze.Analyze', @@ -103,45 +104,49 @@ def test_returns_latest_sell_signal(mocker): return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) ) ) - assert _ANALYZE.get_signal('ETH/BTC', '5m') == (True, False) + assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False) def test_get_signal_empty(default_conf, mocker, caplog): caplog.set_level(logging.INFO) - mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None) - assert (False, False) == _ANALYZE.get_signal('foo', default_conf['ticker_interval']) + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=None) + exchange = get_patched_exchange(mocker, default_conf) + assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval']) assert log_has('Empty ticker history for pair foo', caplog.record_tuples) def test_get_signal_exception_valueerror(default_conf, mocker, caplog): caplog.set_level(logging.INFO) - mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1) + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1) + exchange = get_patched_exchange(mocker, default_conf) mocker.patch.multiple( 'freqtrade.analyze.Analyze', analyze_ticker=MagicMock( side_effect=ValueError('xyz') ) ) - assert (False, False) == _ANALYZE.get_signal('foo', default_conf['ticker_interval']) + assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval']) assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples) def test_get_signal_empty_dataframe(default_conf, mocker, caplog): caplog.set_level(logging.INFO) - mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1) + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1) + exchange = get_patched_exchange(mocker, default_conf) mocker.patch.multiple( 'freqtrade.analyze.Analyze', analyze_ticker=MagicMock( return_value=DataFrame([]) ) ) - assert (False, False) == _ANALYZE.get_signal('xyz', default_conf['ticker_interval']) + assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', default_conf['ticker_interval']) assert log_has('Empty dataframe for pair xyz', caplog.record_tuples) def test_get_signal_old_dataframe(default_conf, mocker, caplog): caplog.set_level(logging.INFO) - mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1) + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1) + exchange = get_patched_exchange(mocker, default_conf) # FIX: The get_signal function has hardcoded 10, which we must inturn hardcode oldtime = arrow.utcnow() - datetime.timedelta(minutes=11) ticks = DataFrame([{'buy': 1, 'date': oldtime}]) @@ -151,15 +156,16 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog): return_value=DataFrame(ticks) ) ) - assert (False, False) == _ANALYZE.get_signal('xyz', default_conf['ticker_interval']) + assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', default_conf['ticker_interval']) assert log_has( 'Outdated history for pair xyz. Last tick is 11 minutes old', caplog.record_tuples ) -def test_get_signal_handles_exceptions(mocker): - mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock()) +def test_get_signal_handles_exceptions(mocker, default_conf): + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock()) + exchange = get_patched_exchange(mocker, default_conf) mocker.patch.multiple( 'freqtrade.analyze.Analyze', analyze_ticker=MagicMock( @@ -167,7 +173,7 @@ def test_get_signal_handles_exceptions(mocker): ) ) - assert _ANALYZE.get_signal('ETH/BTC', '5m') == (False, False) + assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, False) def test_parse_ticker_dataframe(ticker_history): diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index fe07f4dd6..a4096cc01 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -13,6 +13,7 @@ from jsonschema import ValidationError from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration +from freqtrade.constants import DEFAULT_DB_PROD_URL, DEFAULT_DB_DRYRUN_URL from freqtrade.tests.conftest import log_has from freqtrade import OperationalException @@ -152,6 +153,43 @@ def test_load_config_with_params(default_conf, mocker) -> None: assert validated_conf.get('strategy_path') == '/some/path' assert validated_conf.get('db_url') == 'sqlite:///someurl' + conf = default_conf.copy() + conf["dry_run"] = False + del conf["db_url"] + mocker.patch('freqtrade.configuration.open', mocker.mock_open( + read_data=json.dumps(conf) + )) + + arglist = [ + '--dynamic-whitelist', '10', + '--strategy', 'TestStrategy', + '--strategy-path', '/some/path' + ] + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + validated_conf = configuration.load_config() + assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL + + # Test dry=run with ProdURL + conf = default_conf.copy() + conf["dry_run"] = True + conf["db_url"] = DEFAULT_DB_PROD_URL + mocker.patch('freqtrade.configuration.open', mocker.mock_open( + read_data=json.dumps(conf) + )) + + arglist = [ + '--dynamic-whitelist', '10', + '--strategy', 'TestStrategy', + '--strategy-path', '/some/path' + ] + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + validated_conf = configuration.load_config() + assert validated_conf.get('db_url') == DEFAULT_DB_DRYRUN_URL + def test_load_custom_strategy(default_conf, mocker) -> None: """ diff --git a/freqtrade/tests/test_fiat_convert.py b/freqtrade/tests/test_fiat_convert.py index 24f0f776b..2fb9219ca 100644 --- a/freqtrade/tests/test_fiat_convert.py +++ b/freqtrade/tests/test_fiat_convert.py @@ -40,7 +40,8 @@ def test_pair_convertion_object(): assert pair_convertion.price == 30000.123 -def test_fiat_convert_is_supported(): +def test_fiat_convert_is_supported(mocker): + patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert fiat_convert._is_supported_fiat(fiat='USD') is True assert fiat_convert._is_supported_fiat(fiat='usd') is True @@ -48,7 +49,9 @@ def test_fiat_convert_is_supported(): assert fiat_convert._is_supported_fiat(fiat='ABC') is False -def test_fiat_convert_add_pair(): +def test_fiat_convert_add_pair(mocker): + patch_coinmarketcap(mocker) + fiat_convert = CryptoToFiatConverter() pair_len = len(fiat_convert._pairs) @@ -70,11 +73,8 @@ def test_fiat_convert_add_pair(): def test_fiat_convert_find_price(mocker): - api_mock = MagicMock(return_value={ - 'price_usd': 12345.0, - 'price_eur': 13000.2 - }) - mocker.patch('freqtrade.fiat_convert.Market.ticker', api_mock) + patch_coinmarketcap(mocker) + fiat_convert = CryptoToFiatConverter() with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'): @@ -92,17 +92,15 @@ def test_fiat_convert_find_price(mocker): def test_fiat_convert_unsupported_crypto(mocker, caplog): mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[]) + patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0 assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples) def test_fiat_convert_get_price(mocker): - api_mock = MagicMock(return_value={ - 'price_usd': 28000.0, - 'price_eur': 15000.0 - }) - mocker.patch('freqtrade.fiat_convert.Market.ticker', api_mock) + patch_coinmarketcap(mocker) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0) fiat_convert = CryptoToFiatConverter() @@ -172,8 +170,9 @@ def test_fiat_init_network_exception(mocker): assert length_cryptomap == 0 -def test_fiat_convert_without_network(): +def test_fiat_convert_without_network(mocker): # Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap + patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() @@ -186,6 +185,7 @@ def test_fiat_convert_without_network(): def test_convert_amount(mocker): + patch_coinmarketcap(mocker) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0) fiat_convert = CryptoToFiatConverter() diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 381cfc309..1cb2bfca2 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -18,7 +18,7 @@ from freqtrade import constants, DependencyException, OperationalException, Temp from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.state import State -from freqtrade.tests.conftest import log_has, patch_coinmarketcap +from freqtrade.tests.conftest import log_has, patch_coinmarketcap, patch_exchange # Functions for recurrent object patching @@ -32,7 +32,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) - mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) + patch_exchange(mocker) patch_coinmarketcap(mocker) return FreqtradeBot(config) @@ -47,7 +47,7 @@ def patch_get_signal(mocker, value=(True, False)) -> None: """ mocker.patch( 'freqtrade.freqtradebot.Analyze.get_signal', - side_effect=lambda s, t: value + side_effect=lambda e, s, t: value ) @@ -187,9 +187,9 @@ def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None: Test _gen_pair_whitelist() method """ freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.freqtradebot.exchange.get_tickers', tickers) - mocker.patch('freqtrade.freqtradebot.exchange.exchange_has', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + # mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) # Test to retrieved BTC sorted on quoteVolume (default) whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC') @@ -223,7 +223,7 @@ def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mock patch_RPCManager(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2) ) @@ -244,7 +244,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, """ patch_RPCManager(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5) ) @@ -269,7 +269,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), @@ -313,13 +313,13 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: """ patch_RPCManager(mocker) - mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.freqtradebot.Analyze.get_stoploss', MagicMock(return_value=-0.05)) freqtrade = FreqtradeBot(default_conf) # no pair found mocker.patch( - 'freqtrade.freqtradebot.exchange.get_markets', + 'freqtrade.exchange.Exchange.get_markets', MagicMock(return_value=[{ 'symbol': 'ETH/BTC' }]) @@ -329,7 +329,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # no 'limits' section mocker.patch( - 'freqtrade.freqtradebot.exchange.get_markets', + 'freqtrade.exchange.Exchange.get_markets', MagicMock(return_value=[{ 'symbol': 'ETH/BTC' }]) @@ -339,7 +339,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # empty 'limits' section mocker.patch( - 'freqtrade.freqtradebot.exchange.get_markets', + 'freqtrade.exchange.Exchange.get_markets', MagicMock(return_value=[{ 'symbol': 'ETH/BTC', 'limits': {} @@ -350,7 +350,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # empty 'cost'/'amount' section mocker.patch( - 'freqtrade.freqtradebot.exchange.get_markets', + 'freqtrade.exchange.Exchange.get_markets', MagicMock(return_value=[{ 'symbol': 'ETH/BTC', 'limits': { @@ -364,7 +364,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # min cost is set mocker.patch( - 'freqtrade.freqtradebot.exchange.get_markets', + 'freqtrade.exchange.Exchange.get_markets', MagicMock(return_value=[{ 'symbol': 'ETH/BTC', 'limits': { @@ -378,7 +378,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # min amount is set mocker.patch( - 'freqtrade.freqtradebot.exchange.get_markets', + 'freqtrade.exchange.Exchange.get_markets', MagicMock(return_value=[{ 'symbol': 'ETH/BTC', 'limits': { @@ -392,7 +392,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # min amount and cost are set (cost is minimal) mocker.patch( - 'freqtrade.freqtradebot.exchange.get_markets', + 'freqtrade.exchange.Exchange.get_markets', MagicMock(return_value=[{ 'symbol': 'ETH/BTC', 'limits': { @@ -406,7 +406,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # min amount and cost are set (amount is minial) mocker.patch( - 'freqtrade.freqtradebot.exchange.get_markets', + 'freqtrade.exchange.Exchange.get_markets', MagicMock(return_value=[{ 'symbol': 'ETH/BTC', 'limits': { @@ -427,7 +427,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocke patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), @@ -465,7 +465,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), @@ -489,7 +489,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, patch_coinmarketcap(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=buy_mock, @@ -516,7 +516,7 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord patch_coinmarketcap(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=buy_mock, @@ -541,7 +541,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), @@ -567,7 +567,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, marke patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), @@ -595,7 +595,7 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), @@ -625,7 +625,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker_history=MagicMock(return_value=20), get_balance=MagicMock(return_value=20), @@ -650,7 +650,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, patch_RPCManager(mocker) patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_markets=markets, @@ -691,7 +691,7 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non patch_RPCManager(mocker) patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_markets=markets, @@ -713,7 +713,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> msg_mock = patch_RPCManager(mocker) patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_markets=markets, @@ -737,7 +737,7 @@ def test_process_trade_handling( patch_RPCManager(mocker) patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_markets=markets, @@ -758,29 +758,32 @@ def test_process_trade_handling( assert result is False -def test_balance_fully_ask_side(mocker) -> None: +def test_balance_fully_ask_side(mocker, default_conf) -> None: """ Test get_target_bid() method """ - freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 0.0}}) + default_conf['bid_strategy']['ask_last_balance'] = 0.0 + freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 20 -def test_balance_fully_last_side(mocker) -> None: +def test_balance_fully_last_side(mocker, default_conf) -> None: """ Test get_target_bid() method """ - freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 1.0}}) + default_conf['bid_strategy']['ask_last_balance'] = 1.0 + freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 10 -def test_balance_bigger_last_ask(mocker) -> None: +def test_balance_bigger_last_ask(mocker, default_conf) -> None: """ Test get_target_bid() method """ - freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 1.0}}) + default_conf['bid_strategy']['ask_last_balance'] = 1.0 + freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.get_target_bid({'ask': 5, 'last': 10}) == 5 @@ -819,8 +822,8 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.freqtradebot.exchange.get_order', return_value=limit_buy_order) - mocker.patch('freqtrade.freqtradebot.exchange.get_trades_for_order', return_value=[]) + mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -853,7 +856,7 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf, Test the exceptions in process_maybe_execute_sell() """ freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.freqtradebot.exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) trade = MagicMock() trade.open_order_id = '123' @@ -884,7 +887,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, patch_get_signal(mocker) patch_RPCManager(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=MagicMock(return_value={ 'bid': 0.00001172, @@ -935,7 +938,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, patch_coinmarketcap(mocker) mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), @@ -995,7 +998,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), @@ -1033,7 +1036,7 @@ def test_handle_trade_experimental( patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), @@ -1065,7 +1068,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), @@ -1096,7 +1099,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe cancel_order_mock = MagicMock() patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old), @@ -1137,7 +1140,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, patch_coinmarketcap(mocker) cancel_order_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_order=MagicMock(return_value=limit_sell_order_old), @@ -1177,7 +1180,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old patch_coinmarketcap(mocker) cancel_order_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old_partial), @@ -1225,7 +1228,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) - handle_timedout_limit_sell=MagicMock(), ) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')), @@ -1265,7 +1268,7 @@ def test_handle_timedout_limit_buy(mocker, default_conf) -> None: patch_coinmarketcap(mocker) cancel_order_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), cancel_order=cancel_order_mock ) @@ -1291,7 +1294,7 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None: cancel_order_mock = MagicMock() patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), cancel_order=cancel_order_mock ) @@ -1317,7 +1320,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc rpc_mock = patch_RPCManager(mocker) patch_coinmarketcap(mocker) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -1334,7 +1337,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc # Increase the price and sell it mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker_sell_up ) @@ -1360,7 +1363,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, patch_coinmarketcap(mocker) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -1376,7 +1379,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, # Decrease the price and sell it mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker_sell_down ) @@ -1401,7 +1404,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, rpc_mock = patch_RPCManager(mocker) patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -1417,7 +1420,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, # Increase the price and sell it mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker_sell_up ) @@ -1443,7 +1446,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, rpc_mock = patch_RPCManager(mocker) patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker, get_fee=fee, @@ -1459,7 +1462,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, # Decrease the price and sell it mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=ticker_sell_down ) @@ -1484,7 +1487,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, patch_coinmarketcap(mocker) mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=MagicMock(return_value={ 'bid': 0.00002172, @@ -1519,7 +1522,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, patch_coinmarketcap(mocker) mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=MagicMock(return_value={ 'bid': 0.00002172, @@ -1551,9 +1554,9 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) + mocker.patch('freqtrade.freqtradebot.Analyze.stop_loss_reached', return_value=False) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=MagicMock(return_value={ 'bid': 0.00000172, @@ -1587,16 +1590,15 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke patch_coinmarketcap(mocker) mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( - 'freqtrade.freqtradebot.exchange', + 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_ticker=MagicMock(return_value={ - 'bid': 0.00000172, - 'ask': 0.00000173, - 'last': 0.00000172 + 'bid': 0.0000172, + 'ask': 0.0000173, + 'last': 0.0000172 }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, - get_markets=markets ) conf = deepcopy(default_conf) @@ -1614,17 +1616,96 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke assert freqtrade.handle_trade(trade) is True +def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> None: + """ + Test sell_profit_only feature when enabled and we have a loss + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': 0.0000172, + 'ask': 0.0000173, + 'last': 0.0000172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + ) + + conf = deepcopy(default_conf) + conf['experimental'] = { + 'ignore_roi_if_buy_signal': True + } + + freqtrade = FreqtradeBot(conf) + freqtrade.create_trade() + + trade = Trade.query.first() + trade.update(limit_buy_order) + patch_get_signal(mocker, value=(True, True)) + assert freqtrade.handle_trade(trade) is False + + # Test if buy-signal is absent (should sell due to roi = true) + patch_get_signal(mocker, value=(False, True)) + assert freqtrade.handle_trade(trade) is True + + +def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, + fee, markets, mocker) -> None: + """ + Test sell_profit_only feature when enabled and we have a loss + """ + patch_get_signal(mocker) + patch_RPCManager(mocker) + patch_coinmarketcap(mocker) + mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': 0.00000172, + 'ask': 0.00000173, + 'last': 0.00000172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + get_markets=markets + ) + + conf = deepcopy(default_conf) + conf['experimental'] = { + 'ignore_roi_if_buy_signal': False + } + + freqtrade = FreqtradeBot(conf) + freqtrade.create_trade() + + trade = Trade.query.first() + trade.update(limit_buy_order) + # Sell due to min_roi_reached + patch_get_signal(mocker, value=(True, True)) + assert freqtrade.handle_trade(trade) is True + + # Test if buy-signal is absent + patch_get_signal(mocker, value=(False, True)) + assert freqtrade.handle_trade(trade) is True + + def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, caplog, mocker): """ Test get_real_amount - fee in quote currency """ - mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) amount = sum(x['amount'] for x in trades_for_order) trade = Trade( pair='LTC/ETH', @@ -1646,12 +1727,12 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): Test get_real_amount - fee in quote currency """ - mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=[]) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) amount = buy_order_fee['amount'] trade = Trade( pair='LTC/ETH', @@ -1677,8 +1758,8 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) trade = Trade( pair='LTC/ETH', @@ -1703,8 +1784,8 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) trade = Trade( pair='LTC/ETH', @@ -1726,8 +1807,8 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order2) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order2) amount = float(sum(x['amount'] for x in trades_for_order2)) trade = Trade( pair='LTC/ETH', @@ -1754,8 +1835,9 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=[trades_for_order]) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', + return_value=[trades_for_order]) amount = float(sum(x['amount'] for x in trades_for_order)) trade = Trade( pair='LTC/ETH', @@ -1782,8 +1864,8 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=[]) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) amount = float(sum(x['amount'] for x in trades_for_order)) trade = Trade( pair='LTC/ETH', @@ -1807,8 +1889,8 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) trade = Trade( pair='LTC/ETH', @@ -1829,7 +1911,7 @@ def test_get_real_amount_open_trade(default_conf, mocker): patch_get_signal(mocker) patch_RPCManager(mocker) patch_coinmarketcap(mocker) - mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) amount = 12345 trade = Trade( pair='LTC/ETH', diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 9640a7350..414dcdbe9 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -13,7 +13,7 @@ from freqtrade.arguments import Arguments from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main, set_loggers, reconfigure from freqtrade.state import State -from freqtrade.tests.conftest import log_has +from freqtrade.tests.conftest import log_has, patch_exchange def test_parse_args_backtesting(mocker) -> None: @@ -70,6 +70,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: Test main() function In this test we are skipping the while True loop by throwing an exception. """ + patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', _init_modules=MagicMock(), @@ -97,6 +98,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: Test main() function In this test we are skipping the while True loop by throwing an exception. """ + patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', _init_modules=MagicMock(), @@ -124,6 +126,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: Test main() function In this test we are skipping the while True loop by throwing an exception. """ + patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', _init_modules=MagicMock(), @@ -151,6 +154,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: Test main() function In this test we are skipping the while True loop by throwing an exception. """ + patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', _init_modules=MagicMock(), @@ -178,6 +182,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: def test_reconfigure(mocker, default_conf) -> None: """ Test recreate() function """ + patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', _init_modules=MagicMock(), diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index c50ad7d2c..30ad239a1 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -14,9 +14,7 @@ def init_persistence(default_conf): init(default_conf) -def test_init_create_session(default_conf, mocker): - mocker.patch.dict('freqtrade.persistence._CONF', default_conf) - +def test_init_create_session(default_conf): # Check if init create a session init(default_conf) assert hasattr(Trade, 'session') @@ -29,20 +27,17 @@ def test_init_custom_db_url(default_conf, mocker): # Update path to a value other than default, but still in-memory conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - mocker.patch.dict('freqtrade.persistence._CONF', conf) init(conf) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' -def test_init_invalid_db_url(default_conf, mocker): +def test_init_invalid_db_url(default_conf): conf = deepcopy(default_conf) # Update path to a value other than default, but still in-memory conf.update({'db_url': 'unknown:///some.url'}) - mocker.patch.dict('freqtrade.persistence._CONF', conf) - with pytest.raises(OperationalException, match=r'.*no valid database URL*'): init(conf) @@ -53,7 +48,6 @@ def test_init_prod_db(default_conf, mocker): conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - mocker.patch.dict('freqtrade.persistence._CONF', conf) init(conf) assert create_engine_mock.call_count == 1 @@ -66,7 +60,6 @@ def test_init_dryrun_db(default_conf, mocker): conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - mocker.patch.dict('freqtrade.persistence._CONF', conf) init(conf) assert create_engine_mock.call_count == 1 diff --git a/requirements.txt b/requirements.txt index 7e01b10aa..51312791e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.14.201 +ccxt==1.14.256 SQLAlchemy==1.2.8 python-telegram-bot==10.1.0 arrow==0.12.1 @@ -12,7 +12,7 @@ scipy==1.1.0 jsonschema==2.6.0 numpy==1.14.5 TA-Lib==0.4.17 -pytest==3.6.1 +pytest==3.6.2 pytest-mock==1.10.0 pytest-cov==2.5.1 hyperopt==0.1 @@ -22,4 +22,4 @@ tabulate==0.8.2 coinmarketcap==5.0.3 # Required for plotting data -#plotly==2.3.0 +#plotly==2.7.0 diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index 9aedbecb9..2f76c1232 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -6,7 +6,8 @@ import sys import os import arrow -from freqtrade import (exchange, arguments, misc) +from freqtrade import (arguments, misc) +from freqtrade.exchange import Exchange DEFAULT_DL_PATH = 'user_data/data' @@ -39,16 +40,21 @@ if args.days: print(f'About to download pairs: {PAIRS} to {dl_path}') + # Init exchange -exchange._API = exchange.init_ccxt({'key': '', - 'secret': '', - 'name': args.exchange}) +exchange = Exchange({'key': '', + 'secret': '', + 'stake_currency': '', + 'dry_run': True, + 'exchange': { + 'name': args.exchange, + 'pair_whitelist': [] + } + }) pairs_not_available = [] -# Make sure API markets is initialized -exchange._API.load_markets() for pair in PAIRS: - if pair not in exchange._API.markets: + if pair not in exchange._api.markets: pairs_not_available.append(pair) print(f"skipping pair {pair}") continue diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index ce1a4b819..7f9641222 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -35,10 +35,10 @@ from plotly import tools from plotly.offline import plot import freqtrade.optimize as optimize -from freqtrade import exchange from freqtrade import persistence from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments +from freqtrade.exchange import Exchange from freqtrade.optimize.backtesting import setup_configuration from freqtrade.persistence import Trade @@ -73,7 +73,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None: # Load the strategy try: analyze = Analyze(_CONF) - exchange.init(_CONF) + exchange = Exchange(_CONF) except AttributeError: logger.critical( 'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',